| 1 | {{{ |
| 2 | #!html |
| 3 | <h1>TME 8 : Méthode de partitionnement de graphe : Min-Cut</h1> |
| 4 | }}} |
| 5 | [[PageOutline]] |
| 6 | |
| 7 | = Objectif = |
| 8 | |
| 9 | On souhaite implanter en langage C un algorithme de partitionnement de graphe utilisant |
| 10 | la méthode Min-Cut, dont le principe a été présenté en cours. Cet algorithme est fortement |
| 11 | inspiré de la méthode publiée par Fiduccia et Mattheyses en 1982. |
| 12 | Cet algorithme est souvent utilisé en CAO micro-électronique pour résoudre des problèmes de |
| 13 | placement : Les noeuds du graphe représentent des cellules précaractérisèes à placer dans un plan, |
| 14 | et l'existence d'une arête entre deux noeuds i et j représente l'existence d'une connection entre |
| 15 | les deux deux cellules i et j. |
| 16 | |
| 17 | = A) Introduction = |
| 18 | |
| 19 | Soit un graphe G(N, H), où N est l'ensemble des noeuds, et H l'ensemble des |
| 20 | arêtes. Une bipartition de G est formée de 2 sous-ensembles L (left) et R |
| 21 | (right) de l'ensemble des noeuds N, dont l'intersection est vide, et l'union |
| 22 | est égale à N. |
| 23 | |
| 24 | En supposant que le nombre de noeud est pair, on dit qu'une bipartition est |
| 25 | équilibrée si |L| = |R| = N/2 |
| 26 | |
| 27 | Dans le contexte du placement de cellules, on part d'une net-list de cellules |
| 28 | précaractérisées, et on cherche à partitionner l'ensemble des cellules |
| 29 | en deux sous-ensembles L et R tels que le nombre de signaux traversant la |
| 30 | frontière entre les deux sous-ensembles soit minimal. |
| 31 | |
| 32 | Nous allons raisonner sur un graphe non-orienté, défini de la façon suivante: |
| 33 | |
| 34 | Les noeuds Ci représentent les cellules de la net-list `(0 < i < N-1)`. |
| 35 | Il existe une arête entre deux cellules Ci et Cj chaque fois qu'il existe au |
| 36 | moins un signal connectant un port de la cellule Ci à un port de la cellule Cj. |
| 37 | |
| 38 | Attention: avec cette définition, à un seul signal de la net-list peuvent |
| 39 | correspondre plusieurs arêtes dans le graphe: si la sortie d'une |
| 40 | cellule attaque n autres cellules, on a n arêtes dans le graphe. |
| 41 | |
| 42 | On appelle «matrice d'adjacence» {Aij} la matrice carrée N * N qui représente |
| 43 | la structure du graphe: |
| 44 | * Aij = 1 si il existe une arête dans le graphe entre les noeuds Ci et Cj |
| 45 | * Aij = 0 sinon |
| 46 | |
| 47 | = B) Algorithme !MinCut glouton = |
| 48 | |
| 49 | On défini comme suit la fonction de coût du problème d'optimisation !MinCut, |
| 50 | dont la valeur dépend du nombre d'arcs qui traversent la frontière : |
| 51 | {{{ |
| 52 | FC = 1/2 Somme Aij * Kij |
| 53 | i,j |
| 54 | |
| 55 | Kij = 1 si les cellules Ci et Cj sont de part et d'autre de la frontière |
| 56 | Kij = 0 sinon |
| 57 | }}} |
| 58 | |
| 59 | On part d'une bi-partition équilibrée initiale arbitraire en deux ensembles |
| 60 | L et R, et on cherche à optimiser la fonction de coût FC, sans garantie d'atteindre |
| 61 | le minimum absolu, mais avec un temps de calcul très court. |
| 62 | |
| 63 | L'idée de base de l'algorithme est de pré-calculer, pour chaque cellule Ci |
| 64 | un gain G(i) représentant la variation de la fonction de coût FC lorsqu'on |
| 65 | déplace la cellule Ci d'un côté de la frontière à l'autre (sans déplacer |
| 66 | aucune autre cellule). |
| 67 | |
| 68 | Le gain G(i) est défini par: |
| 69 | {{{ |
| 70 | G(i) = Somme Aij * Dij |
| 71 | j |
| 72 | Dij = +1 si les cellules Ci et Cj sont de part et d'autre de la frontière |
| 73 | Dij = -1 sinon |
| 74 | }}} |
| 75 | Après avoir calculé le gain G(i) pour toutes les cellules Ci, on classe les |
| 76 | cellules dans deux listes ordonnées par gain décroissant (une liste ordonnée GAIN_L |
| 77 | pour les cellules appartenant à L et une liste ordonnée GAIN_R pour les cellules |
| 78 | appartenant à R). |
| 79 | |
| 80 | Un mouvement élémentaire consiste à échanger deux cellules i et j : |
| 81 | * une cellule i appartenant à l'ensemble R passe dans l'ensemble L |
| 82 | * une cellule j appartenant à l'ensemble L passe dans l'ensemble R |
| 83 | |
| 84 | La variation de la fonction de coût résultant du mouvement des cellules i et j |
| 85 | peut être calculée de la façon suivante: |
| 86 | {{{ |
| 87 | DFC(i<->j) = G(i) + G(j) - 2 * Aij |
| 88 | }}} |
| 89 | |
| 90 | L'algorithme d'optimisation proposé est "glouton", car il n'y a pas de retour en arrière: |
| 91 | Lorsqu'un mouvement a été accepté, les deux cellules déplacées i et j ne sont plus autorisées à bouger. |
| 92 | |
| 93 | On considère la cellule i appartenant à R possédant le gain G(i) maximum, et la cellule j appartenant à L possédant le gain G(j) maximum. |
| 94 | On calcule la variation de la fonction de coût associé à l'échange i <-> j, et on accepte le mouvement |
| 95 | tant que celui-ci n'entraîne pas une augmentation de la fonction de coût. |
| 96 | |
| 97 | = C) Structures de données = |
| 98 | |
| 99 | On a besoin de deux structures de données séparées pour représenter d'une part |
| 100 | le graphe à partitionner, et d'autre part les listes GAIN_L et GAIN_R. Cette |
| 101 | seconde structure de donnée étant utilisée pour calculer les mouvements de |
| 102 | cellules à effectuer, est appelée «gestionnaire de mouvements». |
| 103 | |
| 104 | == C1) représentation du graphe == |
| 105 | |
| 106 | Les noeuds du graphe sont identifiés par un index compris entre 0 et (NBNODE-1). |
| 107 | |
| 108 | La structure du graphe est représentée par un tableau NODE[ ] indexé par |
| 109 | le numéro du noeud (la taille du tableau est donc égale à NBNODE). Chaque |
| 110 | case du tableau NODE[i] contient un pointeur sur une liste de bi-pointeurs |
| 111 | représentant l'ensemble des noeuds adjacents au noeud [i]. Il y a donc un |
| 112 | bipointeur pour chaque arête attachée au noeud[i]. |
| 113 | |
| 114 | Un second tableau PART[ ] est lui aussi indexé par le numéro du noeud. Chaque |
| 115 | case PART[i] contient un entier définissant à quel sous-ensemble appartient |
| 116 | le noeud [i]: (valeur 0 pour l'ensemble R, et valeur 1 pour l'ensemble L). |
| 117 | |
| 118 | Le graphe est donc représenté par la structure suivante: |
| 119 | {{{ |
| 120 | typedef struct graph_t { |
| 121 | unsigned NBNODE; // nombre de noeuds du graphe |
| 122 | bip_t * * NODE; // pointeur sur le tableau NODE[NBNODE] |
| 123 | unsigned * PART; // pointeur sur le tableau PART[NBNODE] |
| 124 | } graph_t |
| 125 | }}} |
| 126 | |
| 127 | [[Image(graph.png, nolink)]] |
| 128 | |
| 129 | == C2) représentation du gestionnaire des mouvements == |
| 130 | |
| 131 | On appelle arité d'un noeud du graphe, le nombre d'arêtes attachées à ce |
| 132 | noeud. Le gain maximum G(i) associé au mouvement d'un noeud [i] est au plus |
| 133 | égal à l'arité du noeud [i]: Ce gain peut être positif ou négatif, mais sa |
| 134 | valeur absolue ne peut pas être supérieure au nombre des noeuds auquel il |
| 135 | est connecté. |
| 136 | Soit GMAX la valeur maximale de l'arité (en considérant tous les noeuds du graphe). |
| 137 | Pour tout noeud [i] du graphe, on a donc -GMAX < G(i) < GMAX |
| 138 | |
| 139 | Pour représenter la liste ordonnée GAIN_L, on effectue une partition de |
| 140 | l'ensemble des cellules appartenant à L, en regroupant dans un même sous |
| 141 | ensemble tous les noeuds possédant le même gain. Ce sous ensemble est |
| 142 | représenté par une liste doublement chaînée, dont chaque élément de type |
| 143 | bichain_t contient un index de noeud. On définit un tableau de pointeurs |
| 144 | GAIN_L[ ] possèdant 2 * GMAX + 1 cases, et indexé par une valeur de gain |
| 145 | comprise entre -GMAX et GMAX. Chaque case correspond à une valeur de gain, et |
| 146 | contient un pointeur sur une des listes doublement chaînées définies ci-dessus. |
| 147 | * Le gain des cellules rattachées à la case d'index 0 est égal à -GMAX |
| 148 | * Le gain des cellules rattachées à la case d'index i est égal à i - GMAX |
| 149 | * Le gain des cellules rattachées à la case d'index 2*GMAX est égal à GMAX |
| 150 | |
| 151 | [[Image(gestionnaire.png, nolink)]] |
| 152 | |
| 153 | On fait la même chose pour représenter la liste GAIN_R. |
| 154 | |
| 155 | Au début de l'algorithme, chaque noeud du graphe est inséré dans la liste |
| 156 | chaînée qui correspond à sa valeur de gain. Au fur et à mesure que les noeuds |
| 157 | sont transférés, les listes se vident, car un noeud qui a subi un mouvement |
| 158 | n'est plus autorisé à bouger. Puisque le gain des noeuds change au cours |
| 159 | de l'algorithme, un même noeud peut être retiré d'une liste et inséré dans |
| 160 | une autre, ce qui justifie l'utilisation de listes doublement chaînées. |
| 161 | |
| 162 | Dans la structure de donnée «graphe_t» représentant le graphe, les noeuds |
| 163 | du graphe sont identifiés par leur index. On a donc besoin d'introduire dans |
| 164 | la structure «move_manager_t» représentant le gestionnaire de mouvements, |
| 165 | un moyen d'obtenir le pointeur sur la structure bichain_t représentant |
| 166 | un noeud du graphe lorsqu'on connait son index. On construit donc un |
| 167 | tableau de pointeurs NODES [ ], indexé par le numéro du noeud. |
| 168 | |
| 169 | On utilise donc les structures suivantes: |
| 170 | |
| 171 | {{{ |
| 172 | typedef struct bichain_t { |
| 173 | bichain_t *NEXT; // pointeur sur le noeud suivant de même gain |
| 174 | bichain_t *PREV; // pointeur sur le noeud précédent de même gain |
| 175 | unsigned INODE; // index du noeud |
| 176 | int IGAIN; // index dans le tableau des gains |
| 177 | unsigned PART; // numero de la partie 0 pour R, 1 pour L |
| 178 | } bichain_t |
| 179 | }}} |
| 180 | {{{ |
| 181 | typedef struct move_manager_t { |
| 182 | int GMAX; // la taille des tableaux GAIN_R et GAIN_L est égale à 2*GMAX+1 |
| 183 | bichain_t **GAIN_L; // pointeur sur le tableau de pointeurs pour la partie gauche |
| 184 | bichain_t **GAIN_R; // pointeur sur le tableau de pointeurs pour la partie droite |
| 185 | int LX; // index de la première case non vide de GAIN_L (init à -1) |
| 186 | int RX; // index de la première case non vide de GAIN_R (init à -1) |
| 187 | bichain_t **NODES; // pointeur sur le tableau de pointeurs sur les noeuds |
| 188 | } move_manager_t |
| 189 | }}} |
| 190 | |
| 191 | = D) Fonctions d'accès aux structures de données = |
| 192 | |
| 193 | On dispose d'un ensemble de fonctions permettant de construire et de manipuler les deux structures de données graph_t et move_manager_t. |
| 194 | |
| 195 | '''graph_t * parse_graph(char *filename)''':: |
| 196 | Cette fonction prend pour argument un nom de fichier au format .dot |
| 197 | représentant le graphe à partitionner, construit en mémoire la structure |
| 198 | graph_t correspondante, et renvoie un pointeur sur cette structure. Si aucune |
| 199 | partition initiale n'est définie dans le fichier .dot, les noeuds d'index |
| 200 | inférieur à NBNODE/2 sont arbitrairement rangés dans le sous-ensemble L, |
| 201 | et les noeuds d'index supérieur à NBNODE/2 sont rangés dans le sous-ensemble R. |
| 202 | |
| 203 | Un exemple de fichier au format .dot est donné ci-dessous. Les noeuds du graphe |
| 204 | sont identifiés par un index, il y a une ligne par arête, les deux sous-graphes |
| 205 | cluster0 et cluster1 définissent la partition initiale et sont optionnels. |
| 206 | |
| 207 | {{{ |
| 208 | graph |
| 209 | { |
| 210 | // la définition des clusters est optionnelle |
| 211 | subgraph cluster0 {0 1 2 3} |
| 212 | subgraph cluster1 {4 5 6 7} |
| 213 | 0--4 |
| 214 | 0--5 |
| 215 | 0--7 |
| 216 | 1--5 |
| 217 | 3--7 |
| 218 | 1--2 |
| 219 | 5--6 |
| 220 | } |
| 221 | }}} |
| 222 | |
| 223 | '''void drive_graph(graph_t *p, char *filename)''':: |
| 224 | Cette fonction prend pour argument un pointeur p sur une structure graph_t, |
| 225 | ainsi que le nom du fichier de sortie, et écrit sur disque un fichier au |
| 226 | format .dot, en conservant dans le fichier les informations de partition |
| 227 | (sous-ensemble L et R). Ce format est affichable sous forme graphique et |
| 228 | permet de visualiser la partition. |
| 229 | |
| 230 | '''bichain_t * cons_bichain (bichain_t *prev, bichain_t *next, unsigned inode, int igain, unsigned part)''':: |
| 231 | Cette fonction alloue la mémoire nécessaire pour créer un objet de type bichain_t appartenant à une liste |
| 232 | doublement chaînée, elle initialise les différents champs de cette structure, |
| 233 | et renvoie un pointeur sur cette structure. |
| 234 | |
| 235 | '''void free_bichain (bichain_t *p)''':: |
| 236 | Cette fonction libère la mémoire allouée lors de la construction de l'objet |
| 237 | bichain_t par la donction cons_bichain(). |
| 238 | |
| 239 | '''move_manager_t * cons_move_manager(graph_t *p)''':: |
| 240 | Cette fonction pend pour argument un pointeur sur la structure représentant |
| 241 | le graphe partitionné, et construit en mémoire la structure move_manager_t |
| 242 | correspondant à cette partition. |
| 243 | |
| 244 | '''void add_node (move_manager_t *mm, bichain_t *node)''':: |
| 245 | Cette fonction prend pour arguments un pointeur sur la structure représentant |
| 246 | le gestionnaire de mouvements et un pointeur sur un élément de type bichain_t |
| 247 | représentant un noeud) dont les champs INODE, IGAIN et PART sont définis. Elle range |
| 248 | ce noeud dans la liste doublement chaînée correspondante, elle met à jour |
| 249 | le tableau NODES et les index LX ou LR. Les champs NEXT et PREV sont initialisés par la fonction add_node(). |
| 250 | |
| 251 | '''bichain_t * del_node(move_manager_t *mm, bichain_t *node)''':: |
| 252 | cette fonction prend pour arguments un pointeur sur la structure représentant |
| 253 | le gestionnaire de mouvements, et un pointeur sur un élément de liste |
| 254 | doublement chaînée (représentant un noeud). Elle retire cet élément de la |
| 255 | liste doublement chaînée, met à jour le chaînage et les index LX et RX, enfin renvoie un pointeur sur la structure bichain_t |
| 256 | pour une éventuelle libération de la mémoire. |
| 257 | |
| 258 | '''int compute_gain (graph_t *g, unsigned index)''':: |
| 259 | Cette fonction prend pour argument un pointeur sur la structure représentant le |
| 260 | graphe, un index désignant un noeud particulier, et calcule le gain associé |
| 261 | à ce noeud, pour la partition courante. |
| 262 | |
| 263 | '''int global_cost (graph_t *g)''':: |
| 264 | Cette fonction prend pour argument un pointeur sur la structure représentant |
| 265 | le graphe, calcule la fonction de coût globale pour la partition courante, |
| 266 | et renvoie la valeur calculée. |
| 267 | |
| 268 | = E) Travail à réaliser = |
| 269 | |
| 270 | Créez un répertoire de travail '''tme8''', et copiez dans ce répertoire tous les fichiers et répertoires qui se trouvent dans |
| 271 | {{{ |
| 272 | /users/enseig/encadr/cao/tme8 |
| 273 | }}} |
| 274 | |
| 275 | Dans un premier temps vous devez utiliser les fonctions qui vous sont fournies pour écrire le programme main() |
| 276 | qui effectue le partitionnement d'un graphe quelconque défini dans un fichier au format .dot. |
| 277 | Dans un deuxième temps vous devrez programmer vous même les différentes fonctions et remplacer |
| 278 | progressivement les fichiers objets fournis par vos propres fichiers objet. |
| 279 | |
| 280 | '''1. Construction et initialisation du gestionnaire de mouvements''' |
| 281 | Ecrivez le programme ''main()'' qui construit en mémoire les deux structures de données |
| 282 | ''graph_t'' et ''move_manager_t''. Vous disposez pour vous aidez d'une fonction ''dump_move_manager()'' |
| 283 | qui affiche le contenu de la structure. |
| 284 | |
| 285 | '''2. Exécution et évaluation''' |
| 286 | Complétez le programme main() en ajoutant la fonction ''mincut(graph_t *gr, move_manager_t *mm)''. |
| 287 | On utilisera la fonction ''global_cost(graph_t *g)'' pour calculer et afficher le coût de la partition avant et après optimisation. |
| 288 | Exécutez cet algorithme sur différents graphes que vous définirez vous-mêmes. |
| 289 | La partition résultante est-elle toujours la partition optimale? Pourquoi? Donnez un contre-exemple. |
| 290 | |
| 291 | '''3. Ecriture de la fonction mincut() ''' |
| 292 | Ecrire en langage C la fonction mincut() qui réalise l'algorithme !MinCut glouton. |
| 293 | Cette fonction contient la boucle réalisant les transferts de cellules tant que le transfert fait décroitre |
| 294 | la fonction de coût. A chaque tour dans cette boucle, une cellule du sous-ensemble L passe dans R, |
| 295 | et une cellule du sous-ensemble R passe dans L. Les deux cellules i et j qui on bougé doivent être retirées |
| 296 | de la structure de donnée ''move_manager'', et les gains de toutes les cellules adjacentes à i et j doivent |
| 297 | être mis à jour. |
| 298 | |
| 299 | '''4. Ecriture des autres fonctions d'accès''' |
| 300 | Ecrire les différentes fonctions d'accès aux structures de données définies |
| 301 | dans la partie D, et validez ces fonctions en les intégrant peu à peu dans |
| 302 | votre programme. |
| 303 | |
| 304 | '''5. construction du graphe en mémoire''' |
| 305 | L'objectif est ici d'écrire la fonction ''parse_graph()'', en utilisant lex et |
| 306 | yacc. Nous vous suggérons de commencer par faire l'hypothèse qu'il n'y a pas de clusters |
| 307 | dans le fichier, puis d'introduire les clusters dans la grammaire de ce petit langage. |
| 308 | |
| 309 | = Compte-Rendu = |
| 310 | |
| 311 | Il ne vous est pas demandé de compte-rendu écrit pour ce TME, mais vous devrez faire une démonstration |
| 312 | de votre code au début du prochain TME. |