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