DocsPipelinePathfinding

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 :

Nœuds (ou sommets)

Les points du graphe. Dans THOR, chaque nœud représente une gare.

Ex: Paris Gare de Lyon, Lyon Part Dieu, Marseille Saint-Charles...
Arêtes (ou liaisons)

Les connexions entre les nœuds. Dans THOR, chaque arête représente une liaison ferroviaire directe.

Ex: Paris ↔ Lyon, Lyon ↔ Marseille...

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.

Paris
117 min
Lyon

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

2,782
Nœuds (gares)
7,852
Arêtes (liaisons)
Temps
Poids (minutes)

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 ?

1
Initialisation
On assigne une distance 0 au nœud de départ, et (infini) à tous les autres.
2
Exploration
On visite le nœud non-visité ayant la plus petite distance. On met à jour les distances de ses voisins si on trouve un chemin plus court.
3
Répétition
On répète l'étape 2 jusqu'à atteindre la destination ou avoir visité tous les nœuds accessibles.
4
Résultat
On remonte le chemin depuis la destination vers l'origine pour obtenir l'itinéraire optimal.

Complexité algorithmique

La complexité mesure les ressources nécessaires (temps, mémoire) en fonction de la taille du problème.

Complexité temporelle
O((V + E) × log V)
Avec V = nombre de nœuds (gares) et E = nombre d'arêtes (liaisons)
Complexité spatiale
O(V)
On stocke la distance minimale connue pour chaque nœud
En pratique pour THOR : Avec 2,782 gares et 7,852 liaisons, le calcul prend environ 5ms.

Exemple détaillé : Paris → Marseille

Suivons l'algorithme de Dijkstra pas à pas pour trouver le trajet Paris → Marseille.

0
Initialisation
Paris
0
Lyon
Marseille
Bordeaux
Lille
On commence avec Paris à distance 0, tous les autres à l'infini.
1
Visite de ParisNœud avec plus petite distance = Paris (0)
Paris
0 ✓
Lyon
117
Marseille
Bordeaux
130
Lille
62
On met à jour les voisins de Paris : Lyon (0+117=117), Bordeaux (0+130=130), Lille (0+62=62)
2
Visite de LilleNœud avec plus petite distance = Lille (62)
Paris
0 ✓
Lyon
117
Marseille
Bordeaux
130
Lille
62 ✓
Lille n'améliore aucun chemin vers nos destinations (pas de TGV direct vers Lyon/Marseille).
3
Visite de LyonNœud avec plus petite distance = Lyon (117)
Paris
0 ✓
Lyon
117 ✓
Marseille
117+100=217
Bordeaux
130
Lille
62 ✓
On met à jour Marseille : 117 (Paris→Lyon) + 100 (Lyon→Marseille) = 217 min
Résultat final
Paris117 minLyon100 minMarseille
Temps total : 217 min (3h37) — Chemin optimal trouvé !

Structures de données utilisées

File de priorité (Min-Heap)

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')
Dictionnaires

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.

Pondération simple (temps réel)
poids=temps_trajet
Paris → Lyon : poids = 117 min
Pondération THOR (avec pénalité)
poids=temps×pénalité
Paris → Lyon TGV : 117 × 1.0 = 117

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.

Exemple : Bordeaux → Marseille
TER direct
6h (360 min)×2.0=720
TGV via Paris
5h30 (330 min)×1.0=330
→ Dans cet exemple, l'algorithme choisit le TGV via Paris (poids 330 < 720)

Tableau des pénalités par type de train

Type de trainMultiplicateurEffet sur le calcul
TGV / OUIGO×1.0Aucune pénalité — temps réel utilisé
Lyria / Eurostar×1.0TGV internationaux — aucune pénalité
Correspondance×1.0Transfert 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 de pondération
# 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 total

Dijkstra 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 ?

Approche naïve

Choisir une gare "par défaut" (ex: la plus grande) → Résultat sous-optimal

Approche THOR (Multi-source)

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.

Gares de Paris testées :
Gare de LyonMontparnasseSaint-LazareNordEstBercyAusterlitz
Gares de Lyon testées :
Part DieuPerracheSaint-Exupéry
Paris Gare de Lyon → Lyon Part Dieu117 min ✓
Paris Gare de Lyon → Lyon Perrache125 min
Paris Bercy → Lyon Part Dieu135 min
Paris Montparnasse → Lyon Perrache145 min
... et 17 autres combinaisons testées
Algorithme Multi-source
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 routes

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

Exemple : Biarritz → Marseille
TGVBiarritz → Paris Montparnasse
CorrespondanceParis Montparnasse → Paris Gare de Lyon (métro, 40-60 min)
TGVParis Gare de Lyon → Marseille

Gares concernées

Les correspondances inter-gare sont définies manuellement pour les principales gares parisiennes :

Paris MontparnasseParis Gare de LyonParis Nord
Les temps de transfert incluent le trajet en métro/RER + marges de sécurité.

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.

❌ Sans exclusion
"Lyon" pourrait retourner Lyon Saint-Exupéry TGV (à 30km du centre)
✓ Avec exclusion
"Lyon" retourne Lyon Part Dieu ou Lyon Perrache (centre-ville)

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 :

"Aéroport""CDG""Charles de Gaulle""Saint-Exupéry""Orly"
Note : Ces gares restent accessibles comme correspondances (passage), mais pas comme origine ou destination finale.
Logique d'exclusion
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 stations

Sé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

+200Nom exact de la gare (ex: cherche "Paris Gare de Lyon" → trouve exactement)
+100Grande ville majeure (Marseille, Lyon, Toulouse, Nice, Bordeaux, Lille...)
+50Gare principale reconnue (Saint-Charles, Part-Dieu, Saint-Jean, Montparnasse...)
+30Gare TGV ou centrale
+2×NNombre de connexions (N) dans le réseau ferroviaire
-20Gare secondaire (banlieue, aéroport, RER...)

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 / PerracheLyon-Dagneux (01)
"paris"Paris Gare de Lyon, Nord...Paris-Plage
Note : Si l'utilisateur cherche explicitement l'homonyme avec tirets (ex: "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-Charles

Configuration

Paramètres du pathfinding
{
  "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

Terminal
python3 -m src.cli.pathfinding \
  --origin "Paris" \
  --destination "Marseille" \
  --model dijkstra

Via Python

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

Route Paris → Lyon
{
  "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.

Format GeoJSON LineString
{
  "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
  ]
}
Format : [longitude, latitude] — Attention, Leaflet utilise [lat, lon] !
~60%
Liaisons avec géométrie
~300
Points par segment (moy.)
3,348
Lignes dans shapes.json

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.

Pourquoi pas Pythagore ?

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.

Précision de Haversine

Haversine suppose une Terre parfaitement sphérique (rayon = 6,371 km). Erreur < 0.5% pour la plupart des calculs.

Formule mathématique
a = sin²(Δlat/2) + cos(lat₁) × cos(lat₂) × sin²(Δlon/2)
d = 2 × R × arctan2(√a, √(1−a))
R = rayon de la Terre (6,371 km),Δlat = différence de latitudes,Δlon = différence de longitudes
Implémentation Python
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 km
Utilisation dans THOR : Haversine est utilisé pour (1) calculer les distances entre gares, (2) matcher les géométries aux liaisons (seuil < 5km), (3) trier les résultats par proximité.

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

1
Pour chaque liaison (ex: Paris → Lyon), on cherche dans shapes.json les lignes passant près des deux gares.
2
On calcule la distance Haversine entre chaque point de la ligne et les coordonnées des gares.
3
Si une ligne passe à moins de 5km des deux gares, on extrait le segment correspondant.
4
La géométrie extraite est ajoutée à 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)