Réétiquetages de graphes dynamiques avec JBotSim Université de Bordeaux 25 novembre 2014 1 Installation de JBotSim Téléchargez la dernière version de JBotSim (wget http://jbotsim.sf.net/jbotsim.jar). Si vous utilisez Eclipse, créez un nouveau projet Java (choisissez un nom puis cliquez sur propriétés, rendez-vous dans Java Build Path > Librairies, et ajouter jbotsim.jar Finish). Dans les comme JAR externe. Si vous n'utilisez un autre IDE, il doit avoir des fonctionalités équivalentes pour ajouter une archive jar dans le classpath. Si vous n'utilisez pas d'IDE, vous pourrez compiler votre programme en tapant et l'exécuter en tapant 2 java -cp .:jbotsim.jar MaClasse. javac -cp jbotsim.jar MaClasse.java Exemple de programme basique Créez une classe principale (appelons-la MainClass) dont la méthode main() a le code suivant : Topology tp = new Topology(); new JViewer(tp); Ce programme créé une nouvelle topologie (objet qui gère les sommets et les arêtes), et la passe comme argument à un visualisateur dont le but est de la dessiner tout en permettant à l'utilisateur d'interagir directement avec elle. En particulier, vous pouvez ajouter des sommets (clique gauche), les supprimer (clique droit), ou encore les déplacer (glisser/déposer). Lancez votre programme et familiarisez-vous quelques instants avec ces possibilités d'interaction. 3 Objectifs du TD L'objectif du TD est de créer un support d'exécution pour algorithmes à base de réétiquetages de graphes, fonc- tionnant en contexte hautement dynamique, c'est à dire en considérant que les changements de connexité dans le graphe sont très fréquents et tout à fait normaux. Comme vu en cours, les réétiquetages permettent d'abstraire la communication entre voisins : au lieu de spécier les messages que les n÷uds s'envoient, on spécie directement les changements d'états résultant d'une interaction. Nous allons tout d'abord créer un ordonnanceur (appelé adversaire dans le cours) pour sélectionner les interactions au fur et à mesure. L'ordonnanceur que nous allons créer est un processus centralisé, qui pioche au hasard les arêtes sur lesquelles l'algorithme travaillera. Nous pourrions tout aussi bien (ou peut-être juste un peu moins bien) utiliser un ordonnanceur distribué, mais ce n'est pas l'objet de cette séance. Une fois l'ordonnanceur créé, nous l'utiliserons pour tester quelques algorithmes à base de réétiquetages de graphes dynamiques, parmi lesquels un algorithme de propagation d'information, trois algorithmes de comptage et un algorithme de maintien de forêt couvrante. 4 Création d'un ordonnanceur Commencez par recopier le squelette d'ordonnanceur ci-dessous. Lors de la construction, cet ordonnanceur prend une topologie en paramètre et s'en copie la référence pour un usage ultérieur. Il souscrit ensuite aux évènements de l'horloge globale de JBotSim (Clock), ce qui aura pour conséquence l'invocation de sa méthode onClock() à intervalles réguliers (ici, toutes les 10 unités de temps, une unité valant 10ms par défaut). → Travail à faire : remplir la méthode onClock() de sorte à sélectionner une arête aléatoire parmi toutes les arêtes de la topologie (tp.getLinks()). À ce stade, nous nous bornerons à mettre cette arête en gras (par exemple setWidth(3) pour 3 pixels de large). Pensez aussi à remettre l'épaisseur de la dernière arête sélectionnée à 1. Enn, n'oubliez pas 1 public class Scheduler implements ClockListener{ Topology tp; public Scheduler(Topology tp){ this.tp = tp; Clock.addClockListener(this, 10); } public void onClock() { } } // TODO : remplir cette fonction pour selectionner une interaction au hasard qu'il est possible que le graphe soit sans arête ! Testez votre programme. Si les arêtes clignotent comme prévu et que cela vous convainc que l'ordonnanceur fonctionne, vous pouvez passer à l'exercice suivant. 5 Algorithme 1 : propagation d'information Notre premier algorithme est extrêmement basique : si un n÷ud informé (état informé (état N), il lui transmet l'information (réétiquetage du second n÷ud en I). I) interagit avec un n÷ud non- Ce n÷ud pourra ensuite, à son tour, la transmettre aux autres n÷uds. Algorithm 1 Propagation d'une information dans le réseau Etats initiaux : I pour le sommet source ; Règle de réétiquetage : I N N pour tous les autres. I I 5.1 Implémentation générique des algorithmes An de ne pas réinventer la roue pour chaque algorithme que l'on codera, nous allons dénir une interface générique qui permettra à l'ordonnanceur de manipuler un algorithme. Chaque algorithme devra implémenter cette interface en en redénissant les méthodes. Algorithm 2 L'interface Algorithm import jbotsim.Link; import jbotsim.Node; public interface Algorithm { public void setDefaultState(Node node); // Initialise les noeuds ``normaux'' public void setDistinguishedState(Node node); // Initialise le noeud ``distingue'' (si applicable) public void applyNormalRule(Link link); // Realise l'interaction sur l'arete selectionnee. } Via cet interface, notre algorithme pourra dénir l'état à donner aux n÷uds par défaut, par exemple, pour l'Algorithme 1, vous redénirez la méthode setDefaultState(Node node), qui consistera en l'instruction node.setState("N"). La seconde méthode est utilisée pour aecter un état spécial à l'un des n÷uds, par exemple ici, l'état d'émetteur initial représenté par l'étiquette "I". La troisième méthode encapsulera l'exécution de la règle à proprement parler, par exemple ici, tester si l'une des extrémités est à "N" et l'autre à "I", et si tel est le cas, modier le "N" en "I". Reste maintenant à modier l'ordonnanceur pour qu'il invoque ces trois méthodes au bon moment. Remarque sur l'usage des couleurs : A la place des étiquettes "N" et "I", vous pouvez utiliser la coloration des n÷uds pour indiquer leur état. Cela se fait en utilisant setColor(), forme de chaîne de caractère, par exemple blue, green, etc. 2 avec pour argument le nom de la couleur sous 5.2 Modication de l'ordonnanceur Jusqu'à présent nous n'avions qu'un argument au constructeur de la classe Scheduler. En plus de la topologie, nous voulons maintenant recevoir une référence vers l'algorithme à exécuter. An d'être générique, nous allons donc recevoir un argument de type Algorithm et conserver sa référence dans une variable globale algo pour un usage ultérieur (comme pour la topologie). Nous ajouterons également à la n du constructeur le bloc d'instruction suivant : Node model = new Node(); // Declare un noeud (qui ne sera PAS ajoute a la topologie) algo.setDefaultState(model); // Donner l'etat par defaut a ce noeud Node.setModel("default", model); // Dire a JBotSim que ce noeud servira de modele pour les nouveaux noeuds Ainsi, tout noeud créé pendant l'exécution (par exemple, lorsque vous faites un clique gauche sur la topologie) recevra l'état de ce noeud ctif, en l'occurrence, l'état par défaut de l'algorithme considéré. Prenez quelques secondes pour bien comprendre l'enchainement de ces appels. Nous voulons maintenant permettre à l'utilisateur de désigner le noeud émetteur, celui qui a l'information ini- JBotSim tialement. permet à l'utilisateur de sélectionner un n÷ud en cliquant dessus avec la molette (clique Scheduler implémente l'interface SelectionListener tp.addSelectionListener(this) dans le constructeur. Il ne restera plus qu'à implémenter la méthode nodeSelected() an de récupérer la référence du noeud cliqué pour lui aecter l'état distingué de l'algorithme (algo.setDistinguishedState()). Vériez que ces initialisations fonctionnent avant de passer à la suite. milieu). Pour récupérer cet évènement, il faut que notre classe et rajouter On y est presque, encore un petit eort ! Nous sommes en train de construire des briques génériques qui nous feront ensuite gagner beaucoup de temps. Jusqu'à présent, nous avons fait en sorte que les nouveaux noeuds créés recoivent l'état par défaut de l'algorithme (quel que soit cet algorithme !) et que les noeuds cliqués avec la molette en recoivent l'état distingué. Il ne nous reste plus qu'à déclencher la règle de réétiquetage à chaque fois qu'une arête est sélectionnée. Je vous laisse deviner quelle ligne il faut rajouter et où il faut la mettre. 5.3 Implémenter un algorithme. Grâce à la généricité de notre code, l'implémentation d'un algorithme particulier devient une tâche facile. Créez une classe → AlgoPropagation qui implémente l'interface Algorithm et remplissez comme il faut ses trois méthodes. Indice : on peut récupérer les références vers les deux n÷uds d'un lien à l'aide de la méthode Link. Cette méthode renvoie une liste qui n'a endpoints().get(0), et l'autre via endpoints().get(1). la classe endpoints() de que deux éléments. L'un des n÷uds sera donc accédé via Faîtes fonctionner votre algorithme et vériez bien que tout fonctionne comme vous l'attendiez. Bouger les n÷uds pendant l'exécution en rééchissant à la condition nécessaire pour que tous les n÷uds recoivent l'information. 6 Algorithmes de comptage On veut maintenant que les n÷uds parviennent à se compter mutuellement. On s'intéressera d'abord au cas où un n÷ud désigné se charge de compter tous les autres, puis on regardera des solutions de comptage où tous les n÷uds démarrent avec le même état (initialisation uniforme). → Travail à faire : implémenter les trois algorithmes de comptage suivants. Pour chaque algorithme, on s'interrogera, lors de l'exécution, sur les conditions nécessaires à son succès en terme de propriétés temporelles sur la topologie (existence de trajets, etc.). Algorithm 3 Comptage du nombre de sommets (avec compteur distingué) Etats initiaux : C,1 pour le sommet source ; Règle de réétiquetage : C, i N N pour tous les autres. C, i + 1 F Par exemple ici, quelle propriété minimale doit avoir le graphe dynamique pour permettre le succès de cet algorithme, 1) en supposant un choix heureux de compteur ? ou 2) en supposant n'importe quel choix de compteur ? Algorithm 4 Comptage du nombre de sommets (avec compteurs fusionnants) Etats initiaux : 1 pour tous les sommets. Règle de réétiquetage : i 6= 0 j 6= 0 i+j 0 3 Donner une condition nécessaire au succès de cet algorithme. Existe-t-il une condition susante ? Si vous deviez choisir entre cet algorithme et le précédent, lequel utiliseriez-vous ? Algorithm 5 Comptage du nombre de sommets (avec compteurs fusionnants et circulants) Etats initiaux : 1 Règle de fusion : pour tous les sommets. i 6= 0 Règle de circulation : j 6= 0 i+j i 6= 0 0 0 0 i De manière générale, lorsque qu'un algorithme est composé de plusieurs règles, on choisira le comportement suivant (cette stratégie vaudra également pour le dernier algorithme du TD : celui du maintien d'une forêt d'arbres couvrants). Une fois l'arête sélectionnée par l'ordonnanceur, on tente d'abord d'exécuter la première règle de la liste (p.ex. ici, la règle de fusion). Si cette règle est applicable, alors elle est appliquée et l'ordonnanceur reprend la main. Sinon, on tente d'appliquer la seconde règle sur cette même arête, etc. 7 Règles de réparation (exemple de la forêt couvrante) Nous considérons maintenant l'exemple d'un algorithme capable de réagir à la disparition d'une arête inci- dente. L'algorithme consiste à maintenir une forêt d'arbres couvrants qui supporte n'importe quel type d'événement ajout ou suppression de sommets ou d'arêtes et n'importe quelle fréquence de ces événements. L'objectif est de rester cohérent (i.e. pas de cycle et une seule racine par arbre) quitte à mettre plus longtemps à converger vers un état optimal (un arbre par composante connexe). Cet algorithme est constitué de trois règles de réétiquetage, deux desquelles sont du genre habituel : une règle de fusion, s'appliquant entre deux racines d'arbres et ayant pour eet de fusionner les deux arbres sur l'arête correspondante. Cette fusion crée également une orientation sur l'arête pour indiquer la relation parent/enfant dans cette nouvelle partie de l'arbre. (On représentera cette orientation par une référence du noeud parent sur le noeud enfant.) une règle de circulation, consistant à faire circuler une racine au sein de son propre arbre, en inversant la relation parent/enfant locale à chaque mouvement. Ces règles sont d'un genre habituel dans le sens où elles impliquent deux sommets voisins suite à une sélection d'arête. On ajoute maintenant une troisième règle qui ne dépend pas de l'ordonnanceur. Cette règle spécie un traitement à eectuer immédiatement après qu'une arête ait disparue, sur les sommets impliqués. Au niveau sémantique, un sommet qui perd l'arête vers son parent, se reconvertit instantanément en racine. Algorithm 6 Maintien d'une forêt d'arbres couvrants Etats initiaux : R Règle de fusion : pour tous les sommets. R Règle de circulation : R R N Règle de réparation : R N × N N R R ConnectivityListener. Concrètement, l'algorithme se mettra addConnectivityListener sur la topologie. Le code corlinkRemoved(). Il consistera à tester si l'arête perdue menait La troisième règle sera codée en utilisant l'interface à l'écoute de l'apparition/disparition d'arêtes en invoquant respondant à la troisième règle sera alors inséré dans localement vers son parent, et si oui, à régénérer une racine. Encore une fois, n'hésitez pas à vous référer à la javadoc ou à m'appeler si vous avez des questions. → Note : En guise d'arête orientée, vous pouvez simplement mémoriser la référence du n÷ud parent sur le n÷ud enfant. JBotSim ne permet pas de dessiner une arête orientée (à ce jour), mais vous pouvez toutefois épaissir les arêtes qui font partie de l'arbre pour les distinguer des autres, ce qui combiné à une couleur distincte pour la racine, permet de suivre l'exécution de l'algorithme. Il faudra pour y voir plus clair désactiver l'épaississement des arêtes sélectionnées par l'ordonnanceur. 4