AC20/rapport.md

15 KiB

title author UV semester date UVManager UVFollower
La programmation orientée Agent Bartuccio Antoine AC20 TC04 Printemps 2017 FLESCH Alexis GAUD Nicolas

Découvrir un nouveau paradigme

Les paradigmes dans les langages de programmation

Il y a de nombreuses manières de penser un programme informatique et c'est pour cela qu'il existe de nombreux langages pour les écrire. Au delà de la différence de syntaxe, des idiomes et des philosophies différentes, autre chose change : le ou les paradigmes. Le paradigme correspond à la manière de formuler la solution à son problème dans un langage de programmation et influe grandement sur l'esprit du développeur lors de la conception de son algorithme.

Il existe de nombreux paradigme. Le plus simple (pour la machine) est la programmation impérative que l'on retrouve en assembleur ainsi qu'en C. On retrouve également la programmation orientée objet (POO) plus récente que la procédurale, très répandue et très appréciée. Il est tout à fait possible de mélanger différents paradigme dans le même langage de programmation comme par exemple en Python où il n'est pas choquant d'appeler des objets en plein milieu d'un programme pensé de manière procédurale.

Découvrir un nouveau paradigme permet de penser ses programmes différemment, de penser à de nouvelles méthodes de résolution d'un algorithme, penser plus loin que ce à quoi on est déjà habitué, de réfléchir à ses pratiques et de ce fait de les remettre en question.

L'orienté agent

Ici, nous allons nous intéresser à la programmation orientée agent.

La programmation orientée agent (POA) se base sur le concept d'agent logiciel. L'POA est une abstraction de la POO offrant des agents capable d'agir indépendamment et d’échanger des messages entre eux pour interagir et atteindre leur objectif. L'agent tire parti du multitâche en créant automatiquement des sous-processus propre à chaque agent et crée des espaces de communication pouvant recevoir des événements normalisés.

Le choix du SARL

Il est possible de faire de la POA avec un langage orienté objet classique. Cependant, cela nécessite un important travail d'abstraction et d'implémentation. Même après cela, utiliser l'agent n'est ni intuitif ni naturel pour le programmeur. C'est comme faire de l'objet dans un langage structuré comme le C, c'est possible mais pénible.

Il existe très peu de langages nativement orientés agent. Le choix était donc aisé puisqu'un langage développé par l'UTBM existe, il s'agit du SARL.

Le SARL est construit sur le Java. C'est un langage multi-paradigme puisqu'il supporte entre autres l'agent et l'objet. Le langage est construit autour de la bibliothèque Janus qui fourni les bases de l'POA en Java. De plus, puisque SARL est trans-pillé en Java, il peut faire appel à des objets écrits pour le Java, ce qui permet d'accéder à de très nombreuses librairies et de travailler efficacement.

En plus du paradigme différent, le SARL propose une architecture d'agent assez intéressante. Les agents sont holoniques, ils peuvent contenir leurs propre agents et leur propre espace de communication. En plus de cela, grâce à la plate-forme Janus, les agents peuvent être redistribués sur le réseau et être exécutés sur différentes machines.

L'agent en SARL

En SARL, un agent peut, comme un simple objet, contenir des attributs et des méthodes. Cependant, celui-ci possède son propre processus et peut percevoir des événements grâce à ses perceptions. Ainsi, un agent peut réagir ou non à un événement donné selon les conditions (garde) choisi par le programmeur. Un agent peut réagir au même événement avec plusieurs perceptions différentes qui s'exécuteront simultanément.

Initialisation d'un agent simple pouvant recevoir un événement de renommage

agent MyAgent {
	var name : String

	on Initalize {
		name = "Bob"
	}

	on Rename [name !== null]{
		name = "Jean Charles"
	}

}

Un agent peut également obtenir des capacités étendant ses fonctionnalités. Un agent peut écrire dans les logs, émettre des événement dans l'espace où il est enregistré, détecter la présence d'autres agents, accéder à son espace interne…

Exemple d'utilisation de l'utilisation de la capacité logging

import io.sarl.core.Logging

agent Steeve {

	uses logging

	on Hack {
		println("Haked")
	}
}

Un agent peut, comme expliqué précédemment, posséder son propre contexte interne. À l’intérieur de ce contexte interne, il est possible d'instancier d'autres agents qui eux mêmes possèdent leur propre contexte interne. Cela permet une organisation holonique de son application.

Exemple d'agent qui crée (spawn) un nouvel agent dans son contexte interne :

import io.sarl.core.InnerContextAcess

agent Smith {
	uses InnerContextAccess

	on CreateInnerAgent {
		spawnInContext(Smith, innerContext)
	}
}

Les événements

Comme dit précédemment, les agents communiquent à l'aide d'événements. Les événements peuvent s'apparenter à des appels de méthodes en POO. Cependant, l'événement est bien plus puissant. Il peut, au travers de la plate-forme Janus, se propager sur le réseau et interagir avec les agents partagés.

/* Déclaration d'un événement simple */
event MyEvent

Un événement est également capable de transmettre des informations. Il est également possible pour l'agent de récupérer des informations sur l'émetteur de l'événement.

/* Événement possédant des propriétés */
event CoolEvent {
	var name : String
	var nb : Integer

	/* Constructeur de l'événement */
	new(_name : String, _nb : Integer) {
		name = _name
		nb = _nb
	}
}

Un événement atteint par défaut tous les agents présents sur l'espace d'émission. Il est également possible pour un agent de s'envoyer un événement à lui même (avec la méthode wake qui est très rapide) ou à un agent en particulier si son ID est connu.

agent EventSender {
	on Initialize {
		emit(new MyEvent)
		wake(new CoolEvent("Cool", 7))
	}
}

Puisqu'un événement est envoyé sur un espace, il faut comprendre comment ceux-ci fonctionnent pour gérer efficacement les communications entre agents. Un espace est contenu dans un contexte. Un contexte peut contenir n espaces. Chaque contexte contient un espace par défaut. Il existe un contexte par défaut généré lors de la création d'une application SARL. Il est possible de créer autant de contexte que l'on souhaite et un contexte peut être contenu dans un agent.

Penser agent dans son application

Une architecture adaptée

Des agents seuls, part nature, ne peuvent constituer une application complète. Cependant, il est possible de s'en servir comme d'un noyau applicatif sur lequel lire des informations et communiquer. L'interface utilisateur serait un simple objet qui communiquerai avec les agents.

Il existe plusieurs manières d'agencer son application pour parvenir à cela, celle retenu est la méthode Modèle-Vue-Contrôleur (MVC). L'idée est de séparer le Modèle (ici les agents) de la vue (GUI) ainsi que du Contrôleur qui ordonne le modèle et la vue.

Le modèle MVC mis en place dans ce projet

Prise en main du langage

Démo de feux d'artifices

Le but de cette démo est de faire apparaître des feux d'artifices à partir d'agents SARL. L'application est découpée en 4 agents distincts et le tout est construit sur une structure holonique.

Organisation des agents pour la démo de feux d'artifices{ width=150px, height=300px }

L'agent principal est la LaunchingArea qui fait le lien entre la GUI et le reste des agents SARL. C'est elle qui va créer les RocketLauncher, un lanceur par roquette demandé par l'utilisateur, et leur transmettre la gravité et autres paramètres. Cet agent se charge aussi d'enregistrer la GUI sur un espace de communication dédié.

agent LaunchingArea {

	...

	/*
	 * Réception de l'événement de configuration venant de la GUI
	 */
	on SetupSettings {
		this.rocketsQuantity = occurrence.rocketsQuantity
		this.fireQuantity = occurrence.fireQuatity
		this.gravity = occurrence.gravity
		this.maxWidth = occurrence.maxWidth
	}

	/*
	 * Initialisation de l'agent lorsqu'il
	 * 		est généré par la GUI
	 * Un espace de communication est ouvert
	 * 		entre la GUI et l'agent
	 */
	on Initialize [!occurrence.parameters.empty] {
		var ctrl = occurrence.parameters.get(0) as FXMLViewerController
		var ispace = defaultContext.createSpace(
			OpenEventSpaceSpecification, UUID.randomUUID)
		ctrl.setGUISpace(ispace)
		ispace.register(asEventListener)

		ctrl.listenAndDraw(grid)

		info("Finishing initialization of Launching Area")

	}

	...
}

Rendu de l'application sur la GUI{ width=350px, height=300px }

Viennent ensuite les RocketLauncher qui chacun font apparaître une Rocket. Dès que celle-ci est détruite, le RocketLauncher va en générer une nouvelle. Cela permet de déporter la logique de vérification d'existence des Rocket hors de la LaunchingArea et d'isoler les Rocket, et ainsi d'éviter que les événements émis par celles-ci ne viennent perturber leur fonctionnement.

agent RocketLauncher {

	...

	/*
	 * Lancement d'une nouvelle roquette lorsque
	 * 		la précédente est détruite
	 */
	on MemberLeft [!isFromMe(occurrence) && !exited] {
		// wake permet d'envoyer l'événement à l'agent lui même
		wake(new Launch)
	}
	...
}

Les Rocket sont au cœur de la démo. Elles lancent chacune une tache à délais fixé où elles mettent à jour leur position et l'inscrivent dans l'objet partagé avec la GUI ici nommé Positions. Une fois leur durée de vie dépassée, elle génèrent dans leur contexte interne des Fire en accord avec la quantité demandé par l'utilisateur et attend leur destruction pour se détruire.

agent Rocket {

	...

	on Initialize {

		...

		/*
		 * Crée une tâche de fond pour mettre à jour
		 *		sa position à délai fixé
		 */
		move = atFixedDelay(
			Configuration.RocketLifeCycleSchedulingRate) [
			try {
				wake(new UpdateRocketPosition);
			} catch (e : Exception) {
				e.printStackTrace
			}
		]
	}

	on UpdateRocketPosition [isFromMe(occurrence) &&
							 !frozen && !exploded] {
		var vect = new Vector(2)
		x = x + speedx
		y = y + speedy
		vect.clear()
		vect.add(x)
		vect.add(y)
		lifetime = lifetime - 10

		/* Mise à jour de l'objet Position */
		if (grid !== null)
			grid.setRocketPosition(id, vect)
		if (lifetime <= 0) {
			exploded = true
			move.cancel(true)
			grid.hideHocketPosition(id)
			wake(new Explode)
		}
	}

	...
}

Les Fire sont les derniers maillons de cette démo. Ils possèdent une liste de positions et sont soumis à la gravité. Tout comme l'agent Rocket, le Fire lance une tâche à délai fixé pour mettre à jour sa position actuelle, la rajouter à la fin de sa liste de positions et rajouter cette liste dans l'objet Positions. Lorsque sa durée de vie est dépassée, le Fire se détruit.

Lorsque la GUI est fermée, un événement d'extinction Exit est envoyé à la launchingArea qui se charge de le transmettre aux agents qu'elle contient et attend leur destruction pour se détruire et ainsi de suite en descendant dans la hiérarchie.

\newpage

Démo de fractales

Cette démo présente des fractales de Sierpinski. Cela correspond à mettre 3 triangles dans un triangle.

Organisation des agents pour la démo de fractales{ width=150px, height=300px }

Pour réaliser cette démo, un seul agent est nécessaire. Le principe est que cet agent principal crée 3 autres agents dans son contexte interne et devient ensuite un simple transmetteur d'événement et ainsi de suite pour chacune des fractales nouvellement crées.

agent Fractal {
	...

	on Initialize {
		if (occurrence.parameters.size >= 2){
			// Si initialisé avec arguments

			screenSurface = occurrence.parameters.get(0) as Square
			positions = occurrence.parameters.get(1) as Positions

			if (occurrence.parameters.size.equals(3)){
				// Si initialisé par la GUI
				var ctrl = occurrence.parameters.get(2) as FXMLViewerController
				guiSpace =  defaultContext.createSpace(
					OpenEventSpaceSpecification, UUID.randomUUID)
				ctrl.setGUISpace(guiSpace)
				guiSpace.register(asEventListener)
			}
		} else {
			// Si initialisé sans arguments
			screenSurface = new Square
			positions = new Positions
		}
		screenWidth = screenSurface.width
		this.generatePoints
	}

	...

}

Rendu de l'application sur la GUI{ width=250px, height=250px }

\newpage

Difficultés rencontrées

Le fait que l'agent s'exécute dans un sous-processus change complètement la manière de penser son application. Lors de la réalisation de la démo de feux d'artifice, il a fallu synchroniser les départs de fusées et les départs de flammes lors de l'explosion. D'habitude, il aurait suffi de calculer les positions de tous les éléments puis de lancer l'affichage. Ici, puisque chaque agent est indépendant, on ne peut que lire les positions et donc les positions d'une roquette changent pendant qu'une autre roquette est générée. Il a donc fallu exploiter les capacités des agents et leur faculté à détecter l'apparition d'un autre agent dans un espace donné.

Pour fermer l'application SARL, il faut tuer tous les agents qu'elle contient. Pour fermer un agent, il faut qu'il ne contienne aucun agent dans son contexte interne. Puisque les deux démos sont basées sur une structure holonique il a fallu trouver un moyen de d'arrêter tous les agents. Il faut ainsi transmettre l'événement d'extinction à tous les agents contenu. La solution retenue a est de tuer l'agent s'il ne contiens aucun autre agent et de retransmettre sinon. Dans ce deuxième cas, l'agent vérifie si il reste des agents dans son contexte interne à chaque sortie d'un agent et de se détruire si il ne reste plus aucun agent.

Lors du développement, il a été fréquent de se heurter à de nombreuses erreurs de refus de tâche au niveau du noyau Janus (bibliothèque permettant de faire de l'agent en java et qui est au cœur de l'implémentation du SARL). Cela s'explique par la relative jeunesse du langage et le fait qu'il soit encore en développement. Cela survient généralement lors de la fermeture d'une des démos ou lors de l'utilisation du bouton stop pour les feux d'artifice. Dans ces situations, des érreurs de tâche refusée empêchent le bon déroulement du programme. Ces erreurs seront réglées dans une future versions de SARL mais m'ont fait perdre plusieurs heures avant de comprendre qu'il n'était pas possible pour moi d'intervenir.

Netographie

Site internet de SARL: sarl.io