Projet : étape 5.2
Statistiques (courbes d'évolution)

Buts: Afficher graphiquement les courbes d'évolution de certains paramètres.

Les mécanismes complexes comme les mutations sont difficiles à visualiser au cours du temps par simple observation du contenu d'une boite de culture. De même pour comparer l'évolution de plusieurs colonies en compétition, il est plus aisé d'avoir recours à des graphes.

Cette étape du projet a pour but de permettre l'affichage des courbes d'évolution relatives à un certain nombre de paramètres, selon le modèle suivant  (encadré rouge):

Modele
De par les choix de conception adopté, les statistiques affichées sont celles de la boite courante.

Comprendre et compléter du code existant est une compétence importante en programmation. Vous allez, lors de cette étape, exercer cette facette de façon un peu plus importante que précédemment.

Pour réaliser cette étape, vous aurez à utiliser une variante des tables associatives (voir map.pdf), les tables associatives non ordonnées. Une unordered_map s'utilisera de façon analogue à une map (la subtilité entre les deux sera discutée prochainement en cours).

Classe Graph

Une classe Graph est fournie dans l'archive relative à cette étape (répertoire src/Stats).

Cette classe permet de dessiner un ensemble de courbes superposées. Chaque courbe est une série de points reliés entre eux et est associée à un titre (une chaîne de caractères).

Une instance de Graph est donc un ensemble de courbes superposées. Elle sera caractérisée par un libellé (aussi une chaîne de caractères).

Vous avez par exemple ci-dessus, un graphe qui superpose 4 séries de points:

Ce graphe aura dans la simulation un libellé (invisible à l'écran) servant à l'identifier (par exemple "General" pour un graphe de statistiques générales, ou "nutrient quantity" pour un graphe spécifique liés aux quantités de nutriments etc.).
Ne pas confondre donc le libellé d'un graphe servant à l'identifier en interne, avec les titres associés à chacune de ses séries de points ("monotrichous", en rouge, dans la fenêtre tirée de l'exemple ci-dessus):
Modele

Pour résumer, un graphe a un libellé et un ensemble de séries de points. Chaque série de points a un titre.

L'idée est de pouvoir afficher plusieurs de ces Graph et de basculer de l'un à l'autre au moyen de nouveaux contrôles (similaire à ceux que vous avez utilisé pour contrôler la température et l'exposant du gradient).

Par exemple, il devra être possible, lorsque le contrôle courant (celui en rouge) est celui des statistiques, de basculer au moyen de PgUp/X vers l'affichage du graphe relatif à la quantité totale de nutriments disponible dans l'assiette courante (associé au libellé nutrient quantity), ce graphe n'aurait par exemple qu'une série de points à afficher (pas de courbes superposées):

Modele
Différentes constantes contenant des chaînes de caractères sont fournies dans le fichier Utility/Constants.hpp. Vous y trouverez notamment la constante s::GENERAL pour le libellé "General".

Il vous est demandé à cette étape de compléter la classe Stats (voir ci-dessous) permettant de gérer tous les graphes que l'on souhaite afficher dans le cadre de cette simulation et d'ajouter la fonctionnalité permettant de basculer d'un graphe à l'autre.

La classe Stats permettra donc d'afficher plusieurs graphes, chaque graphe étant associé à un libellé le caractérisant.

La classe Application contient désormais un attribut de type Stats* et offre un certain nombre de fonctionnalités relatives à cet attribut. Elle réalise également des traitements sur cet attribut (comme appeler sa méthode drawOn).

Ce sont les programmes graphiques, tels que la classe FinalApplication qui, par des appels à la méthode addGraph héritée de Application, vont définir :

C'est la classe FinalApplication qui, par des appels à la méthode addGraph héritée de Application, va définir le libellé associé à un graphe, l'ensemble des séries de points qui sont associés (désignés par leur titre) ainsi que les bornes inférieures et minimales des plages de valeurs associées à ces séries de points.

Jetez un oeil aux lignes 25 et 26 de la méthode onRun de FinalApplication.cpp (pour le moment commentées). Ce sont ces lignes qui sont concrètement en charge de l'ajout des graphes dans ce programme.

La classe Stats

Un objet de la classe Stats est essentiellement caractérisé par un ensemble de graphes (ensemble de std::unique_ptr<Graph>) et un ensemble de libellés associés à ces graphes (des string). Il s'agit d'un objet dessinable et évoluant au cours du temps.

Chaque graphe (et donc chaque libellé aussi) sera associé à un identifiant entier. Par exemple l'identifiant zéro sera associé au graphe ayant pour libellé "general", l'identifiant 1, sera associé au graphe ayant pour libellé "nutrient quantity" etc.

Un objet de type Stats sera aussi caractérisé par l'identifiant actif (celui correspondant à l'option sélectionnée dans les contrôles): la méthode de dessin de Stats ne dessinera que le graphe ayant pour identifiant l'identifiant actif.

Vous compléterez dans la classe Stats :

Indications:

[Question Q5.3] : quelle(s) structure(s) de données choisissez-vous pour l'ensemble des graphes et l'ensemble des titres de la classe Stats. Répondez à cette question dans votre fichier REPONSES et réalisez les ajouts correspondants dans votre code.

Vous compléterez par ailleurs dans la classe Stats :

La méthode Stats::addGraph a pour rôle de :
Afin d'adhérer à la décision de n'afficher que les statistiques de l'assiette courante, la zone d'affichage des statistiques doit être réinitialisée lorsque l'on bascule d'une boite à l'autre. Ceci se fera évidemment au moyen de la méthode reset que vous avez codée pour les statistiques et que vous pouvez invoquer depuis n'importe quelle classe du code au moyen de la tournure:
    getApp().resetStats(); // appel à la méthode Stats::reset
  

Test 18  : Préparation de l'affichage des courbes

Pour tester cette partie vous pouvez continuer à utiliser l'application finale fournie src/Tests/GraphicalTests/FinalApplication.cpp qui peut être lancée au moyen de la cible application. Vous prendrez soin au préalable de décommenter l'ajout des graphes (lignes 25 et 26 de FinalApplication) et de modifier le paramètre de la ligne 23 de false à true (pour activer l'affichage des statistiques).

Notez que la taille de la zone graphique permettant d'afficher les statistiques est configurable dans le fichier app.json :

    "window":{
        "antialiasing level":4,
        "title":"INFOSV Simulation",
        "simulation":{
            "width":700,
            "height":700
        },
        "stats":{
            "width":200 // ICI par exemple mettre 300 à la place de 200
        },
        "control":{
            "width":300
        }
  

Vous devriez alors simplement voir s'afficher les titres associés aux courbes à venir (sans affichage de courbe à proprement parler pour le moment). Par exemple pour le graphe de libellé "general" :

Modele
La touche PgUp/X (ou équivalent) devrait vous permettre de basculer vers le graphe de libellé "nutriment quantity" :
Modele
PgUp/X (et PgDown/Y rebasculer vers le graphe "general").
Le basculement d'un graphe à l'autre au moyen des touches PageDown/Y et PageUp/X se fait au moyen de l'appel aux méthodes previous et next de votre classe Stats.

Données des graphes

Pour voir des courbes s'afficher effectivement, il faut alimenter en données les séries de points associées à chaque graphe. C'est ce qu'il vous est demandé de faire dans ce qui suit.

La méthode Stats::update

Les données ne sont pas mises à jour à chaque update mais tous les sf::seconds(getAppConfig()["stats"]["refresh rate"].toDouble()); secondes.

Inspirez vous de votre travail dans NutrientGenerator pour mettre en place cette contrainte.

L'algorithme pour la mise à jour des données est ensuite le suivant.

Pour chacun des graphes:

  1. calculer la nouvelle donnée à insérer dans chaque série du graphe;
  2. insérer chaque nouvelle donnée dans sa série;

Partons d'un exemple. Supposons que nous soyons en train de mettre à jour le Graph "general" (tel que décrit plus haut). Il est constitué de 5 séries de points: une pour le nombre de bactérie simple, une autre pour le nombre de bactéries à tentacule etc. Ces séries peuvent être schématisées comme ci-dessous:

{ "monotrichous", {p11, p12, ...  p1j}}
{"pilus mediated", {p21, p22, ...  p2j}}
{"group motility", {p31, p32, ...  p3j}}
{"nutriment sources", {p41, p42, ...  p4j}}
{"temperature", {p51, p52, ...  p5j}}
pXj est le nombre d'entités de la série tel que calculé lors de la jième mise à jour (nombre de bactéries simples pour X valant 1, de bactéries à tentacule pour X valant 2 etc.)

Le pas 1 de l'algorithme ci-dessus, doit donc aller calculer l'ensemble

{new_data = { "simple bacteria", p1j+1}, {"twitching bacteria", p2j+1}, ... {"nutriment sources", p4j+1}...}
où chacun des pXj+1 est le nombre d'éléments de chaque type (bactéries simples, nutriments etc.) tel qu'on peut le calculer à l'instant courant à partir du Lab.

Le pas 2 de l'algorithme «injecte» ces données dans le graphe pour mettre à jour les séries:

{ "monotrichous", {p11, p12, ...  p1j+1}}
{"pilus mediated", {p21, p22, ...  p2j+1}}
{"group motility", {p31, p32, ...  p3j+1}}
{"nutriment sources", {p41, p42, ...  p4j+1}}
{"temperature", {p51, p52, ...  p5j+1}}

Le pas 2 de l'algorithme peut être implémenté très simplement au moyen de l'appel Graph::updateData(deltaT, new_data) sur le graphe à mettre à jour, où deltaT est le temps écoulé depuis la dernière mise à jour des donnée et new_data est l'ensemble des nouvelles données pour chaque série (telle que décrit dans l'exemple précédent).

Il vous est donc demandé pour coder l'étape 1 de l'algorithme de mettre en oeuvre une méthode std::unordered_map<std::string, double> Lab::fetchData(const std::string &) prenant en paramètre le titre du graphe (une string) et retournant l'ensemble new_data lui correspondant (demandez-vous également si cette méthode doit être marquée comme étant const ou non).

Par exemple :

[Question Q5.4] quelle méthodes prévoyez-vous d'ajouter/modifier et dans quelles classes pour réaliser les décomptes souhaités et construire les ensembles new_data? en d'autres termes, comment pouvez-vous compter le nombre d'instances d'une certaine classe? Répondez à cette question dans votre fichier REPONSES et réalisez les implémentations nécessaires.

Test 19  : Affichage de courbes d'évolution

Lancez votre simulation comme précédemment et créer différents types ce bactéries. Vous devriez voir les courbes d'évolution des populations et de la quantité de nutriments s'afficher. Créez tout à coup beaucoup de bactéries d'un certain type et vous devriez pouvoir constater que la courbe d'évolution "réagit" en conséquence. Vérifiez que vous parvenez à basculer d'une statistique à l'autre au moyen des touches de contrôles prévues:

Vérifiez que les courbes de statistiques se re-initialisent proprement en passant d'une boite de culture à l'autre.

Test 20  : Ajout de nouvelles courbes (bonus)

Complétez enfin l'affichage des courbes en permettant de faire afficher les courbes d'évolution de paramètres mutables numériques. Par exemple, pour les bactéries à grappins, les longueurs et vitesses moyennes des grappins.

Lancez votre simulation comme précédemment. Vous devriez avoir la possibilité de visualiser l'évolution des paramètres mutables numériques de vos bactéries, dont voici l'exemple pour les bactéries à tentacule :

Modele
Libre à vous ensuite de faire afficher toute statistique qui vous semblerait pertinente. A noter que les lignes 28, 29, et 30 de FinalApplication préparent un peu ce «terrain».

Vous disposez maintenant d'un outil de simulation complet. Essayez de le paramétrer de sorte à trouver les conditions permettant par exemple d'obtenir des fluctuations du nombre des diverses populations mais sans l'extinction de l'une ou l'autre.

Vous pouvez indiquer dans votre fichier README quelles paramétrages initiaux vous permettent de constater telle ou telle évolution des populations à long terme.
Retour à l'énoncé du projet (partie 5)