Aller au contenu

Ordonnancez un projet avec la méthode potentiels-tâches

Votre équipe doit mettre en production un modèle de scoring : collecter les données, les nettoyer, interroger les métiers, construire les variables, entraîner, préparer l’infrastructure, tester, déployer. Le directeur veut une date de livraison, et il veut savoir quelles tâches ne peuvent surtout pas prendre de retard. Où est la marge, et où n’y en a-t-il aucune ?

La méthode potentiels-tâches (ou méthode MPM) répond à ces questions avec un graphe et un calcul de plus longs chemins. C’est l’application la plus directe de tout ce que vous avez vu sur les chemins optimaux, et un grand classique des examens.

Le projet comporte sept tâches, avec leur durée en jours et leurs contraintes :

TâcheDescriptionDuréeContraintes
Acollecte des données4aucune
Bnettoyage3commence au plus tôt à la fin de A
Centretiens avec les métiers2commence au plus tôt 1 jour après le début du projet
Dconstruction des variables5commence au plus tôt à la fin de B et à la fin de C
Eentraînement du modèle3commence au plus tôt à la fin de D
Fpréparation de l’infrastructure6commence au plus tôt à la fin de C ; commence au plus tard 3 jours après le début de B
Gtests et déploiement2commence au plus tôt à la fin de E et à la fin de F

La contrainte « au plus tard » sur F traduit une exigence réelle : l’équipe infrastructure veut connaître l’architecture cible peu après le début du nettoyage, sans quoi elle réaffecte ses ingénieurs.

On associe à chaque tâche ii une inconnue tit_i, sa date de début (on dit son potentiel). On ajoute deux tâches fictives de durée nulle : α\alpha, le début du projet (tα=0t_\alpha = 0), et ω\omega, sa fin. Toute contrainte s’écrit alors comme une inégalité entre deux dates.

Contrainte en françaisInégalitéArc du graphe
jj commence au plus tôt à la fin de iitj−ti≥dit_j - t_i \geq d_ii→ji \to j, valeur did_i
jj commence au plus tôt aa jours après le début de iitj−ti≥at_j - t_i \geq ai→ji \to j, valeur aa
jj commence au plus tard bb jours après le début de iitj−ti≤bt_j - t_i \leq b, soit ti−tj≥−bt_i - t_j \geq -bj→ij \to i, valeur −b-b

Toutes les inégalités ont la forme tj−ti≥v(i,j)t_j - t_i \geq v(i, j), et chacune devient un arc (i,j)(i, j) de valeur v(i,j)v(i, j). Le graphe obtenu est le graphe potentiels-tâches.

Il faut encore dire que tout se passe entre α\alpha et ω\omega : chaque tâche commence après α\alpha (arc α→i\alpha \to i de valeur 0) et se termine avant ω\omega (arc i→ωi \to \omega de valeur did_i). On ne garde que celles qui ne sont pas redondantes : si une tâche a déjà une contrainte de précédence, l’arc depuis α\alpha n’apporte rien.

0143532226-3αACBFDEGω

Un ordonnancement est compatible s’il respecte toutes les inégalités. Or, le long d’un circuit i1→i2→⋯→i1i_1 \to i_2 \to \dots \to i_1, les inégalités s’additionnent : on obtient 0≥0 \geq (valeur du circuit). D’où le critère :

Ici, le seul arc « à rebours » est F → B, et depuis B on ne revient jamais à F (B mène à D, E, G, puis ω\omega) : il n’y a aucun circuit, l’ordonnancement existe.

La date de début au plus tôt de chaque tâche est la plus petite valeur de tit_i qui respecte toutes les contraintes. Chaque arc (i,j)(i, j) impose tj≥ti+v(i,j)t_j \geq t_i + v(i, j) : il faut donc prendre, pour chaque tâche, le maximum sur ses prédécesseurs, ce qui revient à calculer un plus long chemin de α\alpha à chaque tâche :

tj=max⁡i∈Γ−1(j)(ti+v(i,j)),tα=0t_j = \max_{i \in \Gamma^{-1}(j)} \big(t_i + v(i, j)\big), \qquad t_\alpha = 0

On utilise l’algorithme de Bellman du chapitre 7 avec max au lieu de min ; si le graphe est sans circuit, un passage dans l’ordre topologique suffit.

L’arborescence des chemins optimaux garde, pour chaque tâche, l’arc qui a donné le maximum : α→A\alpha \to A, α→C\alpha \to C, C→FC \to F, A→BA \to B, B→DB \to D, D→ED \to E, E→GE \to G, G→ωG \to \omega.

Un chemin critique est un chemin de valeur maximale de α\alpha à ω\omega : sa valeur est exactement la durée minimale TT. Ici, c’est α→A→B→D→E→G→ω\alpha \to A \to B \to D \to E \to G \to \omega, de valeur 0+4+3+5+3+2=170 + 4 + 3 + 5 + 3 + 2 = 17. Les tâches qui s’y trouvent sont les tâches critiques : A, B, D, E, G. Tout retard sur l’une d’elles retarde le projet entier.

00 | 01 | 54 | 43 | 77 | 712 | 1215 | 1517date au plus tôt | au plus tard (jours)chemin critique : 17 joursαACBFDEGω014322−35362
Le chemin critique (en rose) est un plus long chemin de α à ω, de valeur 17. Ses tâches ont la même date au plus tôt et au plus tard.

Pour une durée de projet fixée à TT, la date au plus tard TiT_i d’une tâche est la date de début la plus tardive qui ne retarde pas la fin. Elle se calcule à rebours depuis ω\omega (Tω=TT_\omega = T) :

Ti=min⁡j∈Γ(i)(Tj−v(i,j))T_i = \min_{j \in \Gamma(i)} \big(T_j - v(i, j)\big)

Autrement dit, Ti=T−ℓ(i,ω)T_i = T - \ell(i, \omega), où ℓ(i,ω)\ell(i, \omega) est la valeur du plus long chemin de ii à ω\omega.

Deux marges mesurent la souplesse d’une tâche :

  • la marge totale MTi=Ti−tiMT_i = T_i - t_i : le retard que peut prendre la tâche sans retarder la fin du projet ;
  • la marge libre MLi=min⁡j∈Γ(i)(tj−ti−v(i,j))ML_i = \min_{j \in \Gamma(i)} \big(t_j - t_i - v(i, j)\big) : le retard qu’elle peut prendre sans décaler la date au plus tôt d’aucune autre tâche.

On a toujours 0≤MLi≤MTi0 \leq ML_i \leq MT_i, et les tâches critiques sont exactement celles de marge totale nulle.

TâcheABCDEFG
Au plus tôt tit_i041712315
Au plus tard TiT_i045712715
Marge totale0040040
Marge libre0000040
0123456789101112131415161718jours depuis le début du projetG (2 j)F (6 j)E (3 j)D (5 j)C (2 j)B (3 j)A (4 j)ABCmarge totale 4DEFmarge totale 4GT = 17en rose : les tâches critiques, sans aucune marge
Les tâches critiques s'enchaînent sans temps mort jusqu'au jour 17 ; C et F peuvent glisser de 4 jours chacune sans retarder la fin.

Regardez C : sa marge totale vaut 4, mais sa marge libre est nulle. Pourquoi ? Pour la marge libre, on examine chaque successeur : tD−tC−2=7−1−2=4t_D - t_C - 2 = 7 - 1 - 2 = 4, mais tF−tC−2=3−1−2=0t_F - t_C - 2 = 3 - 1 - 2 = 0. Le moindre retard de C décale donc F (qui commence au plus tôt à la fin de C), même si le projet, lui, n’est pas retardé tant que ce retard ne dépasse pas 4 jours. Pour F, la marge libre vaut min⁡(tG−tF−6,  tB−tF+3)=min⁡(6,4)=4\min(t_G - t_F - 6,\; t_B - t_F + 3) = \min(6, 4) = 4.

012345678910111213CFDC à sa date au plus tôt (jour 1)012345678910111213CFDF est décalée :marge libre de C = 0D ne bouge pas (jusqu’à 4 jours de retard)C retardée de 2 jours
La marge libre de C est nulle : le moindre retard décale F. La fin du projet, elle, ne bouge pas tant que le retard reste sous la marge totale de 4 jours.
def potentiels(taches, arcs):
"""arcs : liste de (i, j, valeur), avec les tâches fictives 'alpha' et 'omega'.
Plus longs chemins par Bellman (max au lieu de min)."""
sommets = ["alpha", *taches, "omega"]
t = {x: float("-inf") for x in sommets}
t["alpha"] = 0
for _ in range(len(sommets)):
change = False
for i, j, v in arcs:
if t[i] + v > t[j]:
t[j], change = t[i] + v, True
if not change:
break
else:
raise ValueError("circuit de valeur positive : aucun ordonnancement")
T = {x: float("inf") for x in sommets}
T["omega"] = t["omega"]
for _ in range(len(sommets)): # au plus tard, à rebours
for i, j, v in arcs:
T[i] = min(T[i], T[j] - v)
marge_libre = {i: min(t[j] - t[i] - v for a, j, v in arcs if a == i) for i in taches}
return t, T, marge_libre
arcs = [("alpha", "A", 0), ("alpha", "C", 1), ("A", "B", 4), ("B", "D", 3), ("C", "D", 2),
("C", "F", 2), ("F", "B", -3), ("D", "E", 5), ("E", "G", 3), ("F", "G", 6), ("G", "omega", 2)]
t, T, ML = potentiels("ABCDEFG", arcs)
print(t["omega"], {x: T[x] - t[x] for x in "ABCDEFG"}, ML) # 17, marges totales, marges libres

La méthode potentiels-tâches est cousine de la méthode PERT, qui place les tâches sur les arcs plutôt que sur les sommets ; les logiciels de gestion de projet affichent le résultat sous forme de diagramme de Gantt, avec le chemin critique en rouge.

  • Chaque tâche ii a une date de début tit_i ; chaque contrainte s’écrit tj−ti≥v(i,j)t_j - t_i \geq v(i, j) et devient un arc (i,j)(i, j) de valeur v(i,j)v(i, j). On ajoute α\alpha et ω\omega et les contraintes implicites non redondantes.
  • « Au plus tard bb jours après le début de ii » donne un arc de jj vers ii, de valeur −b-b.
  • Un ordonnancement existe si et seulement s’il n’y a aucun circuit de valeur strictement positive.
  • Les dates au plus tôt sont des plus longs chemins depuis α\alpha ; la durée minimale est T=tωT = t_\omega ; le chemin critique est un plus long chemin de α\alpha à ω\omega.
  • Les dates au plus tard se calculent à rebours ; la marge totale Ti−tiT_i - t_i protège la fin du projet, la marge libre protège les dates au plus tôt des autres tâches, et 0≤ML≤MT0 \leq ML \leq MT.

Exercice · Traduire des contraintes

facile

Traduisez en inégalités de potentiels, puis en arcs, les contraintes suivantes (les durées sont dP=4d_P = 4 et dQ=6d_Q = 6) :

  1. Q commence au plus tôt à la fin de P.
  2. Q commence au plus tôt 2 jours après le début de P.
  3. Q commence au plus tard 5 jours après le début de P.
  4. Q se termine au plus tard à la fin de P.
Voir la solution
  1. tQ≥tP+4t_Q \geq t_P + 4, soit tQ−tP≥4t_Q - t_P \geq 4 : arc P → Q de valeur 4.
  2. tQ−tP≥2t_Q - t_P \geq 2 : arc P → Q de valeur 2.
  3. tQ≤tP+5t_Q \leq t_P + 5, soit tP−tQ≥−5t_P - t_Q \geq -5 : arc Q → P de valeur −5-5.
  4. La fin de Q est tQ+6t_Q + 6, celle de P est tP+4t_P + 4. On veut tQ+6≤tP+4t_Q + 6 \leq t_P + 4, soit tP−tQ≥2t_P - t_Q \geq 2 : arc Q → P de valeur 2. Attention : avec la contrainte 1, on obtiendrait le circuit P → Q → P de valeur 4+2=6>04 + 2 = 6 \gt 0, donc aucun ordonnancement possible (Q ne peut pas à la fois commencer après la fin de P et finir avant).

En code

def compatible(sommets, arcs):
"""Bellman avec max : False s'il existe un circuit de valeur strictement positive."""
t = {x: 0 for x in sommets}
for _ in range(len(sommets)):
change = False
for i, j, v in arcs: # arc (i, j, v) : t_j - t_i >= v
if t[i] + v > t[j]:
t[j], change = t[i] + v, True
if not change:
return True
return False # ça bouge encore : circuit positif
c1 = ("P", "Q", 4) # 1. Q au plus tôt à la fin de P
c2 = ("P", "Q", 2) # 2. Q au plus tôt 2 jours après le début de P
c3 = ("Q", "P", -5) # 3. Q au plus tard 5 jours après le début de P
c4 = ("Q", "P", 2) # 4. Q finit au plus tard à la fin de P : t_P - t_Q >= 6 - 4
print(compatible("PQ", [c1, c2, c3])) # True
print(compatible("PQ", [c1, c4])) # False : circuit P -> Q -> P de valeur 4 + 2 = 6

Exercice · Raisonner sur les marges

moyen

Dans le projet du fil rouge, les entretiens avec les métiers (C) démarrent avec 3 jours de retard par rapport à leur date au plus tôt. Quelles sont les conséquences ? Même question avec un retard de 5 jours.

Voir la solution

Retard de 3 jours : C commence à tC=4t_C = 4. Comme sa marge libre est nulle, d’autres tâches sont décalées : F ne peut commencer qu’à 4+2=64 + 2 = 6 au lieu de 3. Mais 3≤MTC=43 \leq MT_C = 4, donc la fin du projet n’est pas retardée. On vérifie : D commence à max⁡(4+3,4+2)=7\max(4 + 3, 4 + 2) = 7 (inchangé), F à 6, ce qui respecte la contrainte « au plus tard » (6≤tB+3=76 \leq t_B + 3 = 7), et G à max⁡(15,6+6)=15\max(15, 6 + 6) = 15. Durée : toujours 17 jours.

Retard de 5 jours : 5>MTC=45 \gt MT_C = 4, donc le projet prend 5−4=15 - 4 = 1 jour de retard. Détail : tC=6t_C = 6, donc tF=8t_F = 8 ; la contrainte F → B impose alors tB≥8−3=5t_B \geq 8 - 3 = 5, et D commence à max⁡(5+3,6+2)=8\max(5 + 3, 6 + 2) = 8, E à 13, G à 16, et la fin à 18 jours. Le retard se propage à la fois par D et, de façon plus sournoise, par la contrainte « au plus tard » qui repousse le nettoyage B.

En code

def au_plus_tot(arcs):
"""Dates au plus tôt : plus longs chemins depuis alpha (Bellman avec max)."""
sommets = {x for a in arcs for x in a[:2]}
t = {x: float("-inf") for x in sommets}
t["alpha"] = 0
for _ in range(len(sommets)):
for i, j, v in arcs:
t[j] = max(t[j], t[i] + v)
return t
def projet(retard_C):
return [("alpha", "A", 0), ("alpha", "C", 1 + retard_C), # C démarre retard_C jours plus tard
("A", "B", 4), ("B", "D", 3), ("C", "D", 2), ("C", "F", 2), ("F", "B", -3),
("D", "E", 5), ("E", "G", 3), ("F", "G", 6), ("G", "omega", 2)]
for retard in (0, 3, 5):
t = au_plus_tot(projet(retard))
print(retard, {x: t[x] for x in ["B", "C", "D", "F", "G", "omega"]})
# 0 {'B': 4, 'C': 1, 'D': 7, 'F': 3, 'G': 15, 'omega': 17}
# 3 {'B': 4, 'C': 4, 'D': 7, 'F': 6, 'G': 15, 'omega': 17}
# 5 {'B': 5, 'C': 6, 'D': 8, 'F': 8, 'G': 16, 'omega': 18}

Exercice · Une contrainte de trop

difficile

L’équipe infrastructure ajoute une exigence : F doit commencer au plus tôt 4 jours après la fin de B. Écrivez la nouvelle inégalité et montrez qu’il n’existe plus aucun ordonnancement compatible. Quelle est la plus grande valeur aa telle que « F commence au plus tôt aa jours après le début de B » reste réalisable ?

Voir la solution

« Au plus tôt 4 jours après la fin de B » s’écrit tF≥tB+3+4t_F \geq t_B + 3 + 4, soit tF−tB≥7t_F - t_B \geq 7 : arc B → F de valeur 7. Avec l’arc F → B de valeur −3-3, on obtient le circuit B → F → B de valeur 7−3=4>07 - 3 = 4 \gt 0. En additionnant les deux inégalités : tF−tB≥7t_F - t_B \geq 7 et tB−tF≥−3t_B - t_F \geq -3 donnent 0≥40 \geq 4, ce qui est absurde. Aucun ordonnancement n’est compatible : on demande à F de commencer au moins 7 jours après B, et au plus 3 jours après.

Avec une contrainte tF−tB≥at_F - t_B \geq a, le circuit vaut a−3a - 3. Il faut a−3≤0a - 3 \leq 0, donc a≤3a \leq 3. Pour a=3a = 3, le circuit est de valeur nulle, ce qui est permis : F commence alors exactement 3 jours après B. Dans ce cas, les tâches B et F sont « verrouillées » l’une à l’autre.

En code

def compatible(arcs):
"""Bellman avec max : False s'il existe un circuit de valeur strictement positive."""
sommets = {x for a in arcs for x in a[:2]}
t = {x: 0 for x in sommets}
for _ in range(len(sommets)):
change = False
for i, j, v in arcs:
if t[i] + v > t[j]:
t[j], change = t[i] + v, True
if not change:
return True
return False
projet = [("alpha", "A", 0), ("alpha", "C", 1), ("A", "B", 4), ("B", "D", 3), ("C", "D", 2),
("C", "F", 2), ("F", "B", -3), ("D", "E", 5), ("E", "G", 3), ("F", "G", 6),
("G", "omega", 2)]
print(compatible(projet + [("B", "F", 3 + 4)])) # False : circuit B -> F -> B de valeur 7 - 3 = 4
# F au plus tôt a jours après le début de B : arc B -> F de valeur a
print([a for a in range(7) if compatible(projet + [("B", "F", a)])]) # [0, 1, 2, 3] : a <= 3