Le problème de la circulation

Un parking vélo rempli à 100 % mais inaccessible vaut zéro. M4 s'assure que chaque vélo posé peut être atteint par un humain.

Capacité brute vs capacité accessible

Le constructeur M2/M3 maximise le nombre de vélos posés — la capacité brute. Mais si un rack est coincé derrière trois rangées sans couloir, il est inutile. La capacité accessible correspond au nombre de vélos qui peuvent être atteints depuis une porte via un couloir de circulation de 1,20 m de large (norme PMR).

1,20m

Largeur couloir PMR

0,15m

Résolution de la grille

8

Cellules pour 1,20 m

⚠️
Conséquence : un rack inaccessible reçoit une death penalty dans la fonction objectif. Il est compté comme zéro vélo, quelle que soit sa capacité théorique.

La grille fine

Un maillage à 0,15 m transforme la géométrie continue en un problème de grille.

1

Discrétisation à 0,15 m

Le polygone de la pièce est rasterisé sur une grille carrée de pas 0,15 m. Chaque cellule est soit libre (0) soit occupée par un vélo (1). Les poteaux et murs sont marqués comme obstacles.

2

Fenêtre de 1,20 m = 8 cellules

Le couloir de circulation a une largeur exacte de 1,20 m, soit 8 cellules de la grille. Pour vérifier si un couloir passe, on teste si une fenêtre de 8 cellules de large est entièrement libre.

3

Sommes préfixes 2D

Pour tester rapidement si une fenêtre rectangulaire est libre, on pré-calcule les sommes préfixes 2D de la grille d'occupation. Le test d'une fenêtre se réduit alors à 4 additions au lieu de parcourir 8×N cellules.

# Somme préfixe 2D — test d'une fenêtre en O(1) def window_sum(prefix, r1, c1, r2, c2): return (prefix[r2+1][c2+1] - prefix[r1][c2+1] - prefix[r2+1][c1] + prefix[r1][c1]) # Couloir libre si somme = 0 dans la fenêtre 8×L is_free = window_sum(prefix, row, col, row+7, col+length) == 0

Dijkstra à coût d'occupation

Trouver le chemin de la porte aux racks qui retire le moins de vélos possible.

1

Nœud source = accès porte

Le graphe de circulation est initialisé avec un nœud source placé au centre du nœud d'accès de la porte. C'est le point d'entrée unique pour les usagers.

2

Coût d'une cellule

Chaque cellule de la grille a un coût de traversée : 0 si elle est libre, sinon le nombre de vélos qu'il faudrait retirer pour la libérer. Le chemin optimal est celui qui minimise ce coût total.

3

Départage par longueur

À coût d'occupation égal, on préfère le chemin le plus court. Un petit epsilon (step_eps) est ajouté au coût par cellule traversée pour garantir ce départage sans perturber l'optimisation principale.

4

Optimalité garantie

Dijkstra avec une file de priorité (min-heap) garantit de trouver le chemin qui retire le minimum absolu de vélos. Pas d'heuristique, pas d'approximation — c'est un optimum exact.

Couloir réservé (B) vs carve a posteriori (A)

Deux approches opposées pour combiner pose de vélos et circulation. Les deux sont évaluées, la meilleure est retenue.

Stratégie A — Carve a posteriori

Poser d'abord, tailler ensuite. Le constructeur remplit la pièce au maximum, puis Dijkstra trace un couloir en retirant les vélos gênants.

  • Plus de vélos posés initialement
  • Le couloir retire des places
  • Résultat net parfois inférieur
Stratégie B — Couloir réservé

Réserver d'abord, poser ensuite. Le couloir de 1,20 m est réservé avant la pose. Le constructeur ne remplit que l'espace restant.

  • Moins de vélos posés initialement
  • Aucun vélo retiré
  • Tous les vélos sont accessibles
⚖️
Verdict : le pipeline évalue les deux stratégies et retient celle qui donne la meilleure capacité accessible nette. En pratique, B domine souvent sur les pièces profondes, tandis que A est meilleure sur les pièces larges et peu profondes.

Mutualisation multi-portes

Quand une pièce a plusieurs portes, les couloirs peuvent se partager des tronçons.

🔀
Réseau en Y : pour 2 portes ou plus, les couloirs de circulation forment naturellement un réseau en Y. Les tronçons partagés (communs à plusieurs portes) ont un coût de 0 car ils sont déjà « payés » par le premier couloir. Résultat : une pièce à 3 portes forme un Y où le tronc commun dessert tous les racks centraux.

Algorithme incrémental

Les portes sont traitées séquentiellement. Après le premier Dijkstra, les cellules du couloir tracé sont marquées comme libres à coût 0. Le Dijkstra suivant (pour la deuxième porte) profite automatiquement de ce tronçon gratuit et ne « creuse » que les segments nécessaires pour atteindre les zones non encore desservies.

1 porte

Couloir unique, linéaire. Dessert les racks le long du chemin.

2 portes

Forme en Y ou L selon la géométrie. Tronc commun partagé.

3+ portes

Arbre de desserte. Chaque branche supplémentaire coûte de moins en moins.

Accessibilité par rack

Chaque rack est individuellement testé pour garantir qu'il est atteignable.

1

BFS depuis l'espace libre

Un parcours en largeur (BFS) part de l'espace libre connecté aux portes. Toutes les cellules libres atteignables sont marquées. Ce BFS s'exécute sur la grille après traçage du couloir.

2

Test d'adjacence par rack

Pour chaque rack posé, on vérifie qu'au moins une cellule adjacente (devant, côtés) est dans l'ensemble des cellules libres atteignables. Si oui, le rack est accessible. Sinon, il est isolé.

3

Death penalty

Un rack inaccessible reçoit une pénalité fatale dans la fonction objectif : sa capacité est comptée comme 0. Cela force l'optimiseur NSGA-II (M5) à éviter les configurations qui créent des zones mortes.

💀
Death penalty en détail : la capacité accessible est calculée comme capacité_brute − vélos_inaccessibles. L'objectif f₁ du NSGA-II est de maximiser cette valeur nette, ce qui élimine naturellement les layouts qui sacrifient l'accessibilité pour la densité.

Pénalité de virage

Un couloir tout droit est plus confortable qu'un labyrinthe. M4 peut en tenir compte.

Comment fonctionne la pénalité de virage ? +

Un paramètre optionnel turn_penalty ajoute un coût supplémentaire à chaque changement de direction dans le chemin de Dijkstra. Un virage à 90° coûte turn_penalty ; un demi-tour (180°) coûte 2 × turn_penalty.

Effet : l'algorithme préfère les couloirs rectilignes, même s'ils sont légèrement plus longs. Cela améliore l'ergonomie pour les usagers et facilite le passage avec des vélos chargés.

ParamètreValeur par défautEffet
turn_penalty0.0Désactivé — chemin le plus court uniquement
turn_penalty0.5Légère préférence pour les lignes droites
turn_penalty2.0Forte pénalisation — couloirs quasi-rectilignes
⚙️
Compromis : une pénalité de virage trop élevée peut forcer un détour qui retire plus de vélos qu'un chemin sinueux. Le réglage optimal dépend de la géométrie de la pièce.
Implémentation dans le graphe de Dijkstra +

L'état dans la file de priorité est étendu de (coût, x, y) à (coût, x, y, direction). Quand la direction change entre deux cellules consécutives, le coût de virage est ajouté.

# État étendu : (coût, ligne, colonne, direction) import heapq def dijkstra_with_turns(grid, source, turn_penalty): dist = {} heap = [(0, source[0], source[1], -1)] # dir=-1 : départ while heap: cost, r, c, d = heapq.heappop(heap) if (r, c, d) in dist: continue dist[(r, c, d)] = cost for nd, (dr, dc) in enumerate(DIRS): nr, nc = r + dr, c + dc cell_cost = grid[nr][nc] # 0 si libre turn_cost = turn_penalty if (d >= 0 and nd != d) else 0 heapq.heappush(heap, (cost + cell_cost + turn_cost, nr, nc, nd))