Pathfinding (Dijkstra)
Algorithme de recherche du plus court chemin optimisé pour le réseau ferroviaire français, avec système de pénalités intelligentes favorisant les trains rapides (TGV) et temps de trajet réels.
Concepts de base
Qu'est-ce qu'un graphe ?
Un graphe est une structure mathématique composée de deux éléments :
Les points du graphe. Dans THOR, chaque nœud représente une gare.
Les connexions entre les nœuds. Dans THOR, chaque arête représente une liaison ferroviaire directe.
Qu'est-ce qu'un graphe pondéré ?
Un graphe pondéré est un graphe où chaque arête possède un poids (ou coût). Ce poids représente le "coût" pour parcourir cette liaison.
Dans THOR, le poids = temps de trajet en minutes. L'algorithme cherche donc le chemin avec le temps total le plus court.
Visualisation du graphe THOR
L'algorithme de Dijkstra
L'algorithme de Dijkstra est un algorithme de recherche du plus court chemin dans un graphe pondéré. Il a été inventé par Edsger Dijkstra en 1956.
Comment ça fonctionne ?
0 au nœud de départ, et ∞ (infini) à tous les autres.Complexité algorithmique
La complexité mesure les ressources nécessaires (temps, mémoire) en fonction de la taille du problème.
O((V + E) × log V)O(V)Exemple détaillé : Paris → Marseille
Suivons l'algorithme de Dijkstra pas à pas pour trouver le trajet Paris → Marseille.
Structures de données utilisées
Permet de toujours récupérer le nœud avec la plus petite distance en temps O(log V).
# Python : heapq
import heapq
heap = [(0, 'Paris')] # (distance, nœud)
heapq.heappush(heap, (117, 'Lyon'))
next_node = heapq.heappop(heap) # (0, 'Paris')Stockent les distances connues et le chemin pour reconstruire l'itinéraire.
distances = {
'Paris': 0,
'Lyon': 117,
'Marseille': 217
}
predecesseurs = {
'Lyon': 'Paris',
'Marseille': 'Lyon'
}Système de pondération
Qu'est-ce que la pondération ?
La pondération est le système qui attribue un poids (coût) à chaque liaison. L'algorithme de Dijkstra cherche à minimiser la somme des poids sur tout le trajet.
poids=temps_trajetpoids=temps×pénalitéPourquoi utiliser des pénalités ?
Sans pénalité, l'algorithme choisirait parfois des TER lents mais directs au lieu de TGV avec correspondance. Les pénalités permettent de favoriser les trains rapides, tout en restant flexible si un train "lent" direct est vraiment plus rapide que plusieurs TGV avec correspondances.
Tableau des pénalités par type de train
| Type de train | Multiplicateur | Effet sur le calcul |
|---|---|---|
| TGV / OUIGO | ×1.0 | Aucune pénalité — temps réel utilisé |
| Lyria / Eurostar | ×1.0 | TGV internationaux — aucune pénalité |
| Correspondance | ×1.0 | Transfert inter-gare (métro/RER) |
| Intercités | ×1.3 | +30% sur le temps réel |
| Train de nuit | ×1.5 | +50% sur le temps réel |
| TER / Navette | ×2.0 | ×2 sur le temps réel — forte pénalité |
# Formule appliquée dans THOR :
poids_pondere = temps_reel_minutes × coefficient_penalite
# Exemple 1 : Paris → Lyon en TGV (117 min)
poids = 117 × 1.0 = 117 ✓ Chemin probable
# Exemple 2 : Liaison TER (180 min)
poids = 180 × 2.0 = 360 ✗ Pénalisé (×2)
# L'algorithme choisit TOUJOURS le chemin avec le PLUS PETIT poids totalDijkstra Multi-source
Pourquoi Multi-source ?
Les grandes villes ont plusieurs gares avec des liaisons différentes. Par exemple, Paris a 7 gares principales. Si l'utilisateur demande "Paris → Lyon", quelle gare choisir ?
Choisir une gare "par défaut" (ex: la plus grande) → Résultat sous-optimal
Tester toutes les combinaisons et garder la meilleure → Optimal garanti
Exemple : Paris → Lyon
Paris a 7 gares, Lyon en a 3. L'algorithme teste les 21 combinaisons possibles (7×3) et retourne la meilleure.
def find_route(origin_city: str, destination_city: str):
# Récupérer toutes les gares de chaque ville
origin_stations = get_stations_for_city(origin_city) # Ex: 7 gares
destination_stations = get_stations_for_city(dest_city) # Ex: 3 gares
best_route = None
best_time = float('inf')
# Tester toutes les combinaisons
for start in origin_stations: # 7 itérations
for end in destination_stations: # 3 itérations → 21 tests
route = dijkstra(start, end)
if route.total_time < best_time:
best_route = route
best_time = route.total_time
return best_route # Meilleure des 21 routesCorrespondances inter-gare
Qu'est-ce qu'une correspondance inter-gare ?
Certaines villes comme Paris ont plusieurs gares non-connectées directement par voie ferrée. Pour optimiser les trajets, THOR peut proposer des correspondances métro/RER entre ces gares.
Gares concernées
Les correspondances inter-gare sont définies manuellement pour les principales gares parisiennes :
Pénalité des correspondances
Les correspondances inter-gare ont un multiplicateur de 1.0, identique aux TGV. Cela permet à l'algorithme de les considérer comme une option viable sans les pénaliser.
TRAIN_TYPE_PENALTY = {
'TGV': 1.0,
'OUIGO': 1.0,
'Correspondance': 1.0, # ← Traité comme TGV
'Intercités': 1.3,
'TER': 2.0
}Affichage sur la carte
Les correspondances sont affichées différemment des trains :
- Ligne jaune pointillée
- Badge "Correspondance" dans les détails
Exclusion des aéroports
Pourquoi exclure les gares d'aéroport ?
Quand un utilisateur demande "Paris → Lyon", il s'attend à arriver en centre-ville, pas à l'aéroport. Les gares d'aéroport sont donc exclues des origines/destinations par défaut.
Mots-clés utilisés pour la détection
Une gare est considérée comme "aéroport" si son nom contient l'un de ces termes :
AIRPORT_KEYWORDS = ['Aéroport', 'CDG', 'Saint-Exupéry', 'Orly']
def is_airport_station(station_name: str) -> bool:
return any(keyword.lower() in station_name.lower()
for keyword in AIRPORT_KEYWORDS)
def get_stations_for_city(city: str, exclude_airports: bool = True):
stations = find_all_stations(city)
if exclude_airports:
stations = [s for s in stations if not is_airport_station(s.name)]
return stationsSélection intelligente des villes
Problématique des homonymes
La France compte plusieurs villes avec des noms similaires. Par exemple, "Marseille" peut faire référence à :
- Marseille (13) - 2ème ville de France, 870 000 habitants
- Marseille-en-Beauvaisis (60) - Petit village, 800 habitants
Sans système intelligent, l'algorithme pourrait choisir le village par erreur (car plus proche de Paris). THOR utilise un système de scoring pour privilégier automatiquement les grandes villes.
Système de scoring des gares
Exclusion automatique des homonymes
Pour les recherches simples (sans tiret), THOR exclut automatiquement les homonymes indésirables :
| Recherche | ✅ Trouve | ❌ Exclut |
|---|---|---|
"marseille" | Marseille Saint-Charles (13) | Marseille-en-Beauvaisis (60) |
"lyon" | Lyon Part-Dieu / Perrache | Lyon-Dagneux (01) |
"paris" | Paris Gare de Lyon, Nord... | Paris-Plage |
"marseille-en-beauvaisis"), le système trouvera bien cette ville.Exemple de calcul de score
# Recherche: "marseille"
Marseille Saint-Charles:
- ville_nom == "marseille" : +80
- "marseille" in MAJOR_CITIES : +100
- "saint-charles" in gare_nom : +50
- nb_connections × 2 : +120
→ Score total : 350
Marseille-en-Beauvaisis:
- EXCLU automatiquement (homonyme indésirable)
→ Non considéré
✅ L'algorithme choisit Marseille Saint-CharlesConfiguration
{
"pathfinding": {
"path_gares": "data/train_station/dataset_gares.json",
"path_liaisons": "data/train_station/dataset_liaisons_enhanced.json",
"path_shapes": "data/train_station/dataset_liaisons_with_shapes.json",
"mode": "time",
"penalty_system": "enabled",
"exclude_airports": true
}
}Utilisation
Via la CLI
python3 -m src.cli.pathfinding \
--origin "Paris" \
--destination "Marseille" \
--model dijkstraVia Python
from src.pathfinding.models.dijkstra import DijkstraPathfinder
# Initialiser le pathfinder
pathfinder = DijkstraPathfinder()
# Trouver un itinéraire
route = pathfinder.find_route("Paris", "Lyon")
print(route.steps) # ['Paris Gare de Lyon', 'Lyon Part Dieu']
print(route.total_time) # 117
print(route.total_distance)# 390.79
print(route.metadata) # Détails des segments, géométries, etc.Exemple de résultat
{
"origin": "Paris",
"destination": "Lyon",
"steps": ["Paris Gare de Lyon", "Lyon Part Dieu"],
"total_time": 117,
"total_distance": 390.79,
"metadata": {
"origin_uic": "87686006",
"destination_uic": "87723197",
"path_uic": ["87686006", "87723197"],
"segments": [
{
"from": "Paris Gare de Lyon",
"to": "Lyon Part Dieu",
"temps_min": 117,
"distance_km": 390.79,
"nb_trains_jour": 120,
"type_train": "TGV",
"geometry": {
"type": "LineString",
"coordinates": [[2.37396, 48.8447], ...]
}
}
]
}
}Géométries des voies ferrées
Qu'est-ce qu'une géométrie ?
Chaque liaison entre deux gares possède une géométrie : une liste de coordonnées GPS qui trace le parcours réel de la voie ferrée.
{
"type": "LineString",
"coordinates": [
[2.373, 48.844], // Paris Gare de Lyon
[2.401, 48.831], // Point intermédiaire
[2.456, 48.792], // Point intermédiaire
[4.859, 45.760] // Lyon Part Dieu
]
}La distance Haversine
La formule de Haversine calcule la distance entre deux points sur une sphère (la Terre) à partir de leurs coordonnées GPS (latitude, longitude). C'est la distance "à vol d'oiseau" en tenant compte de la courbure terrestre.
La formule de Pythagore (√(x² + y²)) fonctionne sur un plan plat. Mais la Terre est une sphère ! Sur de grandes distances, Pythagore donne des résultats faux.
Haversine suppose une Terre parfaitement sphérique (rayon = 6,371 km). Erreur < 0.5% pour la plupart des calculs.
import math
def haversine(lon1: float, lat1: float, lon2: float, lat2: float) -> float:
"""Calcule la distance en km entre deux points GPS."""
R = 6371 # Rayon de la Terre en km
# Convertir en radians
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
# Différences
dlat = lat2 - lat1
dlon = lon2 - lon1
# Formule de Haversine
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
return R * c # Distance en km
# Exemple : Paris → Lyon
distance = haversine(2.349, 48.853, 4.835, 45.764)
print(f"Distance : {distance:.1f} km") # → 391.2 kmProcessus de matching géométrie ↔ liaison
Les géométries proviennent du fichier shapes.json (RFN). Le script generate_railway_shapes.py associe chaque liaison à sa géométrie.
dataset_liaisons_with_shapes.json.Résumé du Pathfinding THOR
Points forts
- Temps de trajet réels (GTFS)
- Système de pénalités intelligent (favorise TGV)
- Multi-source pour villes à plusieurs gares
- Géométries pour affichage carte
- Latence ~5ms
Données utilisées
- → dataset_gares.json (2,782 gares)
- → dataset_liaisons_enhanced.json (7,852 liaisons)
- → dataset_liaisons_with_shapes.json (géométries)
- → shapes.json (3,348 lignes RFN)