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 fil rouge : mettre un modèle en production
Section intitulée « Le fil rouge : mettre un modèle en production »Le projet comporte sept tâches, avec leur durée en jours et leurs contraintes :
| Tâche | Description | Durée | Contraintes |
|---|---|---|---|
| A | collecte des données | 4 | aucune |
| B | nettoyage | 3 | commence au plus tôt à la fin de A |
| C | entretiens avec les métiers | 2 | commence au plus tôt 1 jour après le début du projet |
| D | construction des variables | 5 | commence au plus tôt à la fin de B et à la fin de C |
| E | entraînement du modèle | 3 | commence au plus tôt à la fin de D |
| F | préparation de l’infrastructure | 6 | commence au plus tôt à la fin de C ; commence au plus tard 3 jours après le début de B |
| G | tests et déploiement | 2 | commence 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.
Des contraintes aux inégalités de potentiels
Section intitulée « Des contraintes aux inégalités de potentiels »Une date par tâche
Section intitulée « Une date par tâche »On associe à chaque tâche une inconnue , sa date de début (on dit son potentiel). On ajoute deux tâches fictives de durée nulle : , le début du projet (), et , sa fin. Toute contrainte s’écrit alors comme une inégalité entre deux dates.
| Contrainte en français | Inégalité | Arc du graphe |
|---|---|---|
| commence au plus tôt à la fin de | , valeur | |
| commence au plus tôt jours après le début de | , valeur | |
| commence au plus tard jours après le début de | , soit | , valeur |
Toutes les inégalités ont la forme , et chacune devient un arc de valeur . Le graphe obtenu est le graphe potentiels-tâches.
Les contraintes implicites
Section intitulée « Les contraintes implicites »Il faut encore dire que tout se passe entre et : chaque tâche commence après (arc de valeur 0) et se termine avant (arc de valeur ). 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 n’apporte rien.
Existe-t-il un ordonnancement ?
Section intitulée « Existe-t-il un ordonnancement ? »Un ordonnancement est compatible s’il respecte toutes les inégalités. Or, le long d’un circuit , les inégalités s’additionnent : on obtient (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 ) : il n’y a aucun circuit, l’ordonnancement existe.
Dates au plus tôt et chemin critique
Section intitulée « Dates au plus tôt et chemin critique »Des plus longs chemins
Section intitulée « Des plus longs chemins »La date de début au plus tôt de chaque tâche est la plus petite valeur de qui respecte toutes les contraintes. Chaque arc impose : il faut donc prendre, pour chaque tâche, le maximum sur ses prédécesseurs, ce qui revient à calculer un plus long chemin de à chaque tâche :
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 : , , , , , , , .
Le chemin critique
Section intitulée « Le chemin critique »Un chemin critique est un chemin de valeur maximale de à : sa valeur est exactement la durée minimale . Ici, c’est , de valeur . 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.
Dates au plus tard et marges
Section intitulée « Dates au plus tard et marges »Au plus tard
Section intitulée « Au plus tard »Pour une durée de projet fixée à , la date au plus tard d’une tâche est la date de début la plus tardive qui ne retarde pas la fin. Elle se calcule à rebours depuis () :
Autrement dit, , où est la valeur du plus long chemin de à .
Marge totale et marge libre
Section intitulée « Marge totale et marge libre »Deux marges mesurent la souplesse d’une tâche :
- la marge totale : le retard que peut prendre la tâche sans retarder la fin du projet ;
- la marge libre : le retard qu’elle peut prendre sans décaler la date au plus tôt d’aucune autre tâche.
On a toujours , et les tâches critiques sont exactement celles de marge totale nulle.
| Tâche | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|
| Au plus tôt | 0 | 4 | 1 | 7 | 12 | 3 | 15 |
| Au plus tard | 0 | 4 | 5 | 7 | 12 | 7 | 15 |
| Marge totale | 0 | 0 | 4 | 0 | 0 | 4 | 0 |
| Marge libre | 0 | 0 | 0 | 0 | 0 | 4 | 0 |
Regardez C : sa marge totale vaut 4, mais sa marge libre est nulle. Pourquoi ? Pour la marge libre, on examine chaque successeur : , mais . 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 .
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 libresLa 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.
En résumé
Section intitulée « En résumé »- Chaque tâche a une date de début ; chaque contrainte s’écrit et devient un arc de valeur . On ajoute et et les contraintes implicites non redondantes.
- « Au plus tard jours après le début de » donne un arc de vers , de valeur .
- 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 ; la durée minimale est ; le chemin critique est un plus long chemin de à .
- Les dates au plus tard se calculent à rebours ; la marge totale protège la fin du projet, la marge libre protège les dates au plus tôt des autres tâches, et .
Exercices
Section intitulée « Exercices »Exercice · Traduire des contraintes
facileTraduisez en inégalités de potentiels, puis en arcs, les contraintes suivantes (les durées sont et ) :
- Q commence au plus tôt à la fin de P.
- Q commence au plus tôt 2 jours après le début de P.
- Q commence au plus tard 5 jours après le début de P.
- Q se termine au plus tard à la fin de P.
Voir la solution
- , soit : arc P → Q de valeur 4.
- : arc P → Q de valeur 2.
- , soit : arc Q → P de valeur .
- La fin de Q est , celle de P est . On veut , soit : arc Q → P de valeur 2. Attention : avec la contrainte 1, on obtiendrait le circuit P → Q → P de valeur , 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 Pc2 = ("P", "Q", 2) # 2. Q au plus tôt 2 jours après le début de Pc3 = ("Q", "P", -5) # 3. Q au plus tard 5 jours après le début de Pc4 = ("Q", "P", 2) # 4. Q finit au plus tard à la fin de P : t_P - t_Q >= 6 - 4print(compatible("PQ", [c1, c2, c3])) # Trueprint(compatible("PQ", [c1, c4])) # False : circuit P -> Q -> P de valeur 4 + 2 = 6compatible <- function(sommets, arcs) { # Bellman avec max : FALSE s'il existe un circuit de valeur strictement positive t <- setNames(rep(0, length(sommets)), sommets) for (passage in seq_along(sommets)) { change <- FALSE for (k in 1:nrow(arcs)) { # arc (i, j, v) : t_j - t_i >= v i <- arcs$i[k]; j <- arcs$j[k] if (t[i] + arcs$v[k] > t[j]) { t[j] <- t[i] + arcs$v[k]; change <- TRUE } } if (!change) return(TRUE) } FALSE # ça bouge encore : circuit positif}
contraintes <- data.frame(i = c("P", "P", "Q", "Q"), j = c("Q", "Q", "P", "P"), v = c(4, 2, -5, 6 - 4)) # contraintes 1, 2, 3, 4compatible(c("P", "Q"), contraintes[1:3, ]) # TRUEcompatible(c("P", "Q"), contraintes[c(1, 4), ]) # FALSE : circuit P -> Q -> P de valeur 6Exercice · Raisonner sur les marges
moyenDans 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 à . Comme sa marge libre est nulle, d’autres tâches sont décalées : F ne peut commencer qu’à au lieu de 3. Mais , donc la fin du projet n’est pas retardée. On vérifie : D commence à (inchangé), F à 6, ce qui respecte la contrainte « au plus tard » (), et G à . Durée : toujours 17 jours.
Retard de 5 jours : , donc le projet prend jour de retard. Détail : , donc ; la contrainte F → B impose alors , et D commence à , 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}au_plus_tot <- function(arcs) { # dates au plus tôt : plus longs chemins depuis alpha (Bellman avec max) sommets <- unique(c(arcs$i, arcs$j)) t <- setNames(rep(-Inf, length(sommets)), sommets); t["alpha"] <- 0 for (passage in seq_along(sommets)) for (k in 1:nrow(arcs)) t[arcs$j[k]] <- max(t[arcs$j[k]], t[arcs$i[k]] + arcs$v[k]) t}
projet <- function(retard_C) data.frame( # C démarre retard_C jours plus tard i = c("alpha", "alpha", "A", "B", "C", "C", "F", "D", "E", "F", "G"), j = c("A", "C", "B", "D", "D", "F", "B", "E", "G", "G", "omega"), v = c(0, 1 + retard_C, 4, 3, 2, 2, -3, 5, 3, 6, 2))
for (retard in c(0, 3, 5)) print(au_plus_tot(projet(retard))[c("B", "C", "D", "F", "G", "omega")])# retard 0 : B 4, C 1, D 7, F 3, G 15, omega 17# retard 3 : B 4, C 4, D 7, F 6, G 15, omega 17# retard 5 : B 5, C 6, D 8, F 8, G 16, omega 18Exercice · Une contrainte de trop
difficileL’é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 telle que « F commence au plus tôt 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 , soit : arc B → F de valeur 7. Avec l’arc F → B de valeur , on obtient le circuit B → F → B de valeur . En additionnant les deux inégalités : et donnent , 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 , le circuit vaut . Il faut , donc . Pour , 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 aprint([a for a in range(7) if compatible(projet + [("B", "F", a)])]) # [0, 1, 2, 3] : a <= 3compatible <- function(arcs) { # Bellman avec max : FALSE s'il existe un circuit de valeur strictement positive sommets <- unique(c(arcs$i, arcs$j)) t <- setNames(rep(0, length(sommets)), sommets) for (passage in seq_along(sommets)) { avant <- t for (k in 1:nrow(arcs)) t[arcs$j[k]] <- max(t[arcs$j[k]], t[arcs$i[k]] + arcs$v[k]) if (all(t == avant)) return(TRUE) } FALSE}
projet <- data.frame(i = c("alpha", "alpha", "A", "B", "C", "C", "F", "D", "E", "F", "G"), j = c("A", "C", "B", "D", "D", "F", "B", "E", "G", "G", "omega"), v = c(0, 1, 4, 3, 2, 2, -3, 5, 3, 6, 2))compatible(rbind(projet, data.frame(i = "B", j = "F", v = 3 + 4))) # FALSE : circuit de valeur 4
# F au plus tôt a jours après le début de B : arc B -> F de valeur aFilter(function(a) compatible(rbind(projet, data.frame(i = "B", j = "F", v = a))), 0:6) # 0 1 2 3