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
21 changes: 21 additions & 0 deletions __main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pathfinder.pathfinder import Pathfinder
from pathfinder.astar import AStar
from pathfinder.spfa import SPFA
from pathfinder.graphs import graph, spfa_graph
from pathfinder.heuristics import heuristics
from pathfinder.city import City
from pathfinder.types import Path

pathfinder = Pathfinder(graph)
pathfinder_a = AStar(graph, heuristics)
pathfinder_spfa = SPFA(spfa_graph)

# path_d: Path = pathfinder.get_shortest_path(City.BORDEAUX, City.STRASBOURG)
# print(f" Dijkstra : {path_d}")

# path_a: Path = pathfinder_a.get_shortest_path(City.BORDEAUX, City.STRASBOURG)
# print(f"A* : {path_a}")

path_spfa: Path = \
pathfinder_spfa.get_shortest_path(City.BORDEAUX, City.STRASBOURG)
print(f"SPFA : {path_spfa}")
Binary file added __pycache__/__main__.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/astar.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/city.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/graphs.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/heuristics.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/pathfinder.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/spfa.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/types.cpython-312.pyc
Binary file not shown.
46 changes: 46 additions & 0 deletions pathfinder/astar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from pathfinder.pathfinder import Pathfinder
from pathfinder.types import Step
from pathfinder.types import Graph, Heuristic


class AStar(Pathfinder):
__heuristics: Heuristic

def __init__(self, graph: Graph, heuristics: Heuristic):
Pathfinder.__init__(self, graph)
self.__heuristics = heuristics

def _lowest_cost_in_discoverd_cities(self) -> Step:
"""Find lowest cost with heuristic to determine the next step"""
shortest_path: Step = {
"city": self._current_step["city"],
"origin": self._current_step,
"bestCost": float('inf')
}

for known_city in self._discovered_steps:
city_with_heuristics: float = known_city["bestCost"]\
+ self.__heuristics[known_city["city"]]
actual_shortest_with_heuristics: float = shortest_path["bestCost"]\
+ self.__heuristics[shortest_path["city"]]

shortest_path = known_city if actual_shortest_with_heuristics > \
city_with_heuristics else shortest_path

# A star must stop when end is discovered and not processed
# TODO: change _can_continue_research instead
if self._end in self._discovered_cities:
for step in self._discovered_steps:
if step["city"] == self._end:
self._discovered_cities.remove(self._end)
self._discovered_steps.remove(step)
self._processed_cities.append(self._end)
self._processed_steps.append(step)

return shortest_path

# Ce code ne fait pas ce qui est souhaité ?
# def _can_continue_research(self) -> bool:
# """Express the condition to continue explore new cities.
# Here, the end city must not be discovered"""
# return self._end not in self._discovered_cities
16 changes: 16 additions & 0 deletions pathfinder/city.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from enum import Enum


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

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

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

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


class Pathfinder():
_processed_steps: list[Step] = []
_processed_cities: list[City] = []
_discovered_steps: list[Step] = []
_discovered_cities: list[City] = []
_start: City
_end: City
_current_step: Step

def __init__(self, graph: Graph):
self._graph = graph

def get_shortest_path(self, start: City, end: City) -> Path:
"""Return the path with the min cost to go from start to end point"""
self._start = start
self._current_step = {
"city": self._start,
"origin": None,
"bestCost": 0
}
self._end = end

self._discovered_cities.append(self._start)
self._discovered_steps.append(self._current_step)

while self._can_continue_research():
self._find_next_step()

path: Path = self._get_path_from_steps()
self._reset()
return path

def _find_next_step(self) -> None:
"""Discover cities around the actual one and compare
discovered cities' cost. The lowest is the next city to visit"""
self._visit_neighbourhood()

# Change the current city from discovered to processed
self._discovered_cities.remove(self._current_step["city"])
self._discovered_steps.remove(self._current_step)
self._processed_cities.append(self._current_step["city"])
self._processed_steps.append(self._current_step)

self._current_step = self._lowest_cost_in_discoverd_cities()

def _visit_neighbourhood(self) -> None:
"""Create or update infos about cities around the current one"""
for city in self._graph[self._current_step["city"]]:
# Ignore the city if it's already processed
if city in self._processed_cities:
continue

cost: float = self._graph[self._current_step["city"]][city]\
+ self._current_step["bestCost"]

# Update origin and cost if it's lower than the previous one
if city in self._discovered_cities:
for known_step in self._discovered_steps:
if known_step["city"] == city:
if known_step["bestCost"] > cost:
known_step["bestCost"] = cost
known_step["origin"] = self._current_step

# If it's a new city, create it's associated step (origin + cost)
else:
new_step: Step = {
"city": city,
"origin": self._current_step,
"bestCost": cost
}
self._discovered_cities.append(city)
self._discovered_steps.append(new_step)

def _lowest_cost_in_discoverd_cities(self) -> Step:
"""Compare the costs to determine the lowest"""

shortest_path: Step = {
"city": self._current_step["city"],
"origin": self._current_step,
"bestCost": float('inf')
}

for known_city in self._discovered_steps:
shortest_path = known_city if shortest_path["bestCost"] > \
known_city["bestCost"] else shortest_path

return shortest_path

def _get_path_from_steps(self) -> Path:
"""Recreate the path going from end to start"""
total_cost: float = float("inf")
cities_on_path: list[City] = [self._end]

for step in self._processed_steps:
if step["city"] == self._end:
total_cost = step["bestCost"]
origin: Step | None = step["origin"]
while origin is not None and origin["city"] != self._start:
cities_on_path.insert(0, origin["city"])
origin = origin["origin"]
cities_on_path.insert(0, self._start)

path: Path = {"total": total_cost, "steps": cities_on_path}
return path

def _can_continue_research(self) -> bool:
"""Express the condition to continue explore new cities.
Here, the end city must not be processed"""
return self._end not in self._processed_cities

def _reset(self) -> None:
"""Clean lists used to explore the graph"""
self._processed_steps = []
self._processed_cities = []
self._discovered_steps = []
self._discovered_cities = []
26 changes: 26 additions & 0 deletions pathfinder/spfa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pathfinder.pathfinder import Pathfinder
from pathfinder.types import City
from pathfinder.types import Graph
from pathfinder.types import Path
from pathfinder.types import Step


class SPFA(Pathfinder):

def __init__(self, graph: Graph):
Pathfinder.__init__(self, graph)

def _lowest_cost_in_discoverd_cities(self) -> Step:
"""Here we don't want the cheapest, but the first not processed city"""
if len(self._discovered_steps) > 0:
return self._discovered_steps[0]
return self._processed_steps[0]
# The funtion works but is apparently not the only thing to change

def _can_continue_research(self) -> bool:
"""Express the condition to continue explore new cities.
Here, the end city must not be processed"""
for city in self._graph:
if city not in self._processed_cities:
return True
return False
18 changes: 18 additions & 0 deletions pathfinder/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import TypedDict
from pathfinder.city import City


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


class Step(TypedDict):
city: City
origin: 'Step | None'
bestCost: float


Graph = dict[City, dict[City, float]]

Heuristic = dict[City, float]
Binary file added tests/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added tests/__pycache__/test_astar.cpython-312.pyc
Binary file not shown.
Binary file added tests/__pycache__/test_pathfinder.cpython-312.pyc
Binary file not shown.
Binary file added tests/__pycache__/test_spfa.cpython-312.pyc
Binary file not shown.