Projet : étape 4.1
Bactéries à flagelles uniques
Perception des nutriments, division et mutation

Buts : Il s'agit ici de compléter le codage des bactéries à flagelle unique en leur permettant de percevoir les nutriments, de se diviser et de muter.

Perception du gradient et basculement

Avec le mode de déplacement que vous avez codé à l'étape précédente, une bactérie à flagelle unique n'avance que droit devant elle. Elle ne peut compter que sur le pur hasard (des nutriments sur sa trajectoire) pour se nourrir en se déplaçant. S'il y peu de sources de nutriments, ses chances de survie sont faibles.

Nous allons maintenant améliorer ce modèle en rendant la bactérie sensible à un gradient de concentration des nutriments.

Au lieu de modéliser des milliers de petites particules émises par une source de nutriments, nous considérerons qu'il y a un gradient de concentration des nutriments proportionnel à la distance au centre de la source. Les bactéries à flagelle unique auront une perception de ce gradient qui pourra induire des changements de direction et influencer leur mode de déplacement.

Score d'une position

La perception du gradient depuis une position donnée p se fera par le biais du calcul d'un score attaché à p.

Le score de p par rapport à une source de nutriments s se calcule comme:

score(p) = taille(s) / (distance(p,centre(s))puissance

S'il y a plusieurs sources de nutriments, le score de p sera le cumul des scores pour chaque source de nutriments.

Ainsi le score sera d'autant plus élevé que l'on est près de plus nombreuses ou de plus grosses sources de nutriments.

Commencez par ajouter à Laboratory une méthode getPositionScore(const Vec2d&) retournant le score d'une position donnée.

[Question Q4.1] Comment devez-vous modifier CultureDish pour mettre en oeuvre cette fonctionnalité ? Répondez à cette question dans votre fichier REPONSES puis implémentez les fonctionnalités suggérées.

Contrôle depuis l'interface graphique

L'attribut puissance fait partie des éléments qu'il est prévu de pouvoir contrôler via l'interface graphique. Il s'agit du paramètre "Gradient exponent" qui s'affiche dans l'interface graphique au-dessous de "Temperature".

Vous procéderez donc de la même manière que vous l'avez déjà fait pour les températures : ajout de méthodes increaseGradientExponent(), decreaseGradientExponent() et getGradientExponent() placées aux bons endroits. Vous aurez également à invoquez ces méthodes de façon appropriée dans Application.hpp. Pensez aussi à adapter la gestion de la touche 'C' (lorsque le fichier de configuration est rechargé il faut recalculer puissance, selon les mêmes modalités que lors de la construction).

Basculement

Pour améliorer sa chance d'accès aux nutriments une bactérie à flagelle unique essayera de basculer (à intervalles de temps réguliers) pour changer sa direction de déplacement.

La probabilité de basculement de départ est une propriété de la bactérie. Cette probabilité va évoluer en fonction de la perception du gradient (c'est-à-dire le score) : plus cette perception est grande et plus la probabilité de basculer va faiblir.

Ainsi, si la bactérie se déplace dans une direction la menant à des nutriments elle aura de moins en moins de chance de basculer. Inversement, si sa perception du gradient faiblit, ses chances de basculer augmenteront.

Il vous est maintenant demandé de compléter la méthode de déplacement de la bactérie à flagelle unique de sorte à ce que, à la suite des traitements existants, il y ait :

  1. tentative de basculement (la bactérie décide si elle doit basculer ou non).
  2. basculement éventuel (si elle a décidé de basculer comment elle le fait).
Les algorithmes associés à la mise en oeuvre de ces traitements sont expliqués ci-dessous.

Tentative de basculement (algorithme)

Voici une version très simple, que vous pourrez améliorer plus tard  :

Soit ancien_score le score de la bactérie lors du pas de simulation précédent et score son score courant.

La probabilité p de basculer est calculée selon une loi de croissance exponentielle:

p = 1 - exp(-t/lambda)
t est le temps écoulé depuis le dernier basculement et lambda est le poids associé à la péjoration ou à l'amélioration du score.

Cette fonction garantit que, la probabilité de basculer est d'autant plus grande que le temps écoulé entre deux basculements est important et que le score s'est péjoré entre deux déplacements de la bactérie.

[Question Q4.2] Quel(s) attributs suggérez-vous d'ajouter à la représentation des bactéries (à flagelle unique ou génériques) pour permettre la mise en oeuvre cet algorithme ? Comment initialiser ces attributs et à quels endroits devez-vous les mettre à jour ?

Explicitez et justifiez vos choix dans votre fichier REPONSES.

Basculement (algorithme)

De multiples variantes sont possibles :

  1. choisir au hasard une direction tirée au hasard.
  2. choisir la meilleure parmi N directions générées aléatoirement.
  3. ...

Pour vous permettre d'expérimenter plusieurs variantes, vous pouvez utiliser les valeurs associées à ["tumble"]["algo"] dans le fichier de configuration.

Par exemple, si la valeur associée à ces étiquettes est "single random vector" exécuter la stratégie 1 ci-dessus, si la valeur est "best of N" choisir la seconde etc.

La fonction Vec2d::fromRandomAngle() permet de générer une direction au hasard.

Pour la stratégie 1 il suffirait d'appeler une fois cette fonction pour mettre en oeuvre le changement de direction.

Pour la stratégie 2, on en génère N (par exemple 20) et on retient celle telle que le Vec2D de coordonnées position_bactérie + direction_aléatoire a le meilleur score.

Intégrez maintenant le basculement au déplacement de la bactérie à flagelle unique.

Test 11 : Déplacement avec perception du gradient

Lancez votre test monotrichousTest et créez des bactéries à flagelle unique au moyen de la touche 'M'. Les bactéries doivent désormais être attirées par les sources de nutriments. Si vous ajoutez beaucoup de bactéries et qu'il n'y a plus suffisamment de sources de nutriments vos bactéries devraient basculer pour tenter d'améliorer leur chance de trouver de quoi se nourrir:
Vous vérifierez l'impact du paramètre "Gradient exponent" (modifiable via le menu graphique). Vérifiez aussi que la valeur de ce paramètre réagit proprement aux modifications du fichier de configuration, en cours d'exécution du programme (modification du fichier .json + utilisation de la touche 'C').

Division et mutation

Nous abordons maintenant les dernières fonctionnalités liées aux bactéries de façon générale, à savoir la division et la mutation. Nous considérons pour simplifier que toutes les bactéries peuvent se diviser et muter selon des modalités que l'on peut considérer comme étant les mêmes pour toutes. Lors de la division, la bactérie va se scinder pour produire une autre bactérie identique (méthode Bacterium* clone());

Mutation

La méthode de mutation d'une bactérie (quelle qu'elle soit) consiste simplement à appeler la méthode mutation sur chacun de ses paramètres mutables. Cela peut induire des mutations effectives de certains paramètres (mais pas forcément).

[Question Q4.3] Dans quelle classe proposez-vous d'ajouter la méthode de mutation ? Explicitez et justifiez votre choix dans votre fichier REPONSES.

Vous considérerez qu'une bactérie à flagelle unique a pour propriétés mutables :

Tous ces paramètres, hormis la couleur, sont des paramètres mutables numériques.

Pour mettre en place la gestion des propriétés mutables, vous commencerez par ajouter à votre hiérarchie de classes de bactéries les méthodes addProperty(const string&, MutableNumber) et getProperty(const string&) permettant d'ajouter à l'ensemble des paramètres mutables numériques de la bactérie une valeur numérique mutable donnée ou de retrouver une valeur mutable associée à une clé donnée.

[Question Q4.4] Dans quelle classe proposez-vous d'ajouter ces méthodes ? Explicitez et justifiez votre choix dans votre fichier REPONSES et codez les méthodes suggérées.

Modifiez le constructeur de la classe MonotrichousBacterium de sorte à ce qu'il lui ajoute les paramètres mutables souhaités comme suit :

N'oubliez pas de remplacer les valeurs artificiellement utilisées pour lambda plus tôt ainsi que le 5 utilisé pour la norme de la vitesse dans getSpeedVector par les valeurs effectives de ces paramètres mutables. Vous retrouverez ces valeurs au moyen de votre méthode getProperty.

Division

La méthode de division est la même pour toutes les bactéries :

  1. Si à un moment donné, le niveau d'énergie dépasse un seuil d'énergie requis pour la division (revoir vos getters), la bactérie se divise.
  2. La division d'une bactérie se fait au moyen de la méthode clone déjà anticipée. La méthode clone produit une bactérie du même type que celle qui se divise (une bactérie à flagelle unique doit produire une bactérie à flagelle unique par exemple). La nouvelle bactérie aura les mêmes caractéristiques que la bactérie d'origine mais moitié moins d'énergie.
  3. La bactérie d'origine perd aussi la moitié de son énergie après la division.
  4. Chaque division comporte des possibilités de mutations pour la nouvelle bactérie (appel à la méthode de mutation sur cette bactérie).
  5. La bactérie clonée change sa direction de déplacement (la façon la plus simple est d'inverser sa direction, mais vous pouvez changer sa direction d'un angle plus faible pour éviter des effets indésirés en cas de clonage au confins de la boîte).

La méthode clone doit donc réaliser une copie polymorphique. L'exercice 1 de la série 21 vous donne des indications à ce propos.

[Question Q4.5] Où choisissez-vous de placer la méthode de division d'une bactérie ? Répondez à cette question dans votre fichier REPONSES en justifiant votre choix.

Complétez maintenant la méthode Bacterium::update de sorte qu'après le test de collision avec les nutriments il y ait appel à la méthode de division (laquelle appellera la méthode de mutation).

Modification de la collection de bactéries en cours de parcours

Nous avons vu à l'étape précédente que modifier la taille d'une collection alors que l'on est en train d'itérer dessus est potentiellement problématique. A l'étape précédente le risque était lié à la suppression des éléments morts ou épuisés. Nous avons ce même problème maintenant mais en raison de l'ajout de nouvelles bactéries lors du clonage.

Pour éviter ce problème nous vous suggérons de d'abord stocker les bactéries créées par clonage dans un vector annexe (attribut de la classe). Vous pourrez fusionner ce vector avec la collection de bactéries avant d'itérer dessus. Ainsi toutes les bactéries obtenues par clonage lors du pas de simulation précédent pourront être intégrées à la simulation).

Cela implique d'être rigoureux pour l'initialisation/la réinitialisation aux bons moments de ce vector annexe.

La fonction append fournie dans Utility.hpp permet d'ajouter le contenu d'un vector à un autre (par exemple append(v1, v2) permet d'ajouter à v2 tous les éléments de v1 en les insérant dans l'ordre à la fin de v2).

Test 12 : Division et mutation

Relancez le test tel que précédemment. Favorisez les conditions d'observation dans le .json (génération rapide de nutriments, simulation accélérée, haite probabilité de mutation des composants de couleur etc.) puis créez une dizaine de bactéries à flagelle unique.

Vous devriez voir croître et décroître les populations : la population croît car les bactéries se divisent. Elles deviennent parfois trop nombreuses pour les nutriments disponibles et la population décroît alors. Au bout d'un certain temps, des mutations de couleur devraient être observables:

← ici la simulation est faite avec ce contenu du fichier app.json
(le fichier app_original.json correspond aux données fournies au départ dans l'archive, si vous souhaitez restituer ce contenu)

On ne peut tester ici visuellement que la mutation de la couleur. Dans la suite du projet vous ajouterez à votre programme le dessin de courbes d'évolution de certains paramètres et il deviendra plus facile d'observer l'évolution des paramètres mutables.


Retour à l'énoncé du projet (partie 4) Module suivant (partie 4.2)