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

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

Certaines données importantes de la simulation, comme l'évolution du nombre d'animaux au cours du temps sont difficiles à visualiser par simple observation du monde simulé. Il est plus aisé pour cela 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  :

Modele

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, une structure de données que vous n'avez pas encore utilisée va s'avérer utile. Il s'agit de la notion de «table associative» (map ou unordered_map en C++) généralise la notion de tableau. En voici une petite présentation avec un exemple. 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 exemple de graphe qui superpose 3 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 "Waves" pour un graphe spécifique liés aux ondes émises 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 ("Scorpions", 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.

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 créer une 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 draw).

Comme annoncé au début de l'étape, le code de cette partie ne redeviendra compilable et exécutable que lorsque vous aurez codé ce qui est nécessaire au Test 29.

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 :

Jetez un oeil aux lignes 141 et 142 de la méthode resetStats de FinalApplication.cpp. 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 unique_ptr<Graph>) et un ensemble de libellés associés à ces graphes (des string). associés à ces graphes (des string). Il s'agit d'un objet dessinable et évoluant au cours du temps.

N'oubliez pas de rajouter #include <memory> si nécessaire pour pouvoir utiliser les smart pointers.

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" et l'identifiant 1 sera associé au graphe de libellé "Waves". Ce dernier permettra de visualiser des statistiques de la vue «modèle neuronal».

Un objet de type Stats sera aussi caractérisé par l'identifiant actif (celui correspondant au graphe visible): la méthode de dessin de Stats ne doit dessiner que le graphe ayant pour identifiant l'identifiant actif.

Vous doterez la classe Stats :

La méthode de dessin de la classe Graph s'appelle évidemment aussi draw.

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

Vous doterez par ailleurs la classe Stats :

La méthode Stats::addGraph a pour rôle de :
Indication: operator[] des map n'est pas const. La méthode at() l'est.

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

Pour tester cette partie, utilisez la cible application associée au programme de test FinalApplication.

Vous devriez alors simplement voir s'afficher l'intitulé des graphes, sans courbes à proprement parler:

Modele
Le basculement d'un graphe à l'autre peut être fait au moyen des touches PageDown/8 et PageUp/9 définies dans Application.cpp et qui font appel aux méthodes previous et next de votre classe Stats.

Données des graphes

Pour voir des courbes s'afficher, 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 getAppConfig().stats_refresh_rate secondes.

L'algorithme pour la mise à jour des données est 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" décrit plus haut. Il est constitué de 3 séries de points: une pour le nombre de scorpions, une autre pour le nombre de lézards etc. Ces séries peuvent être schématisées comme ci-dessous:

{"Scorpions", {p11, p12, ...  p1j}}
{"Lizards",  {p21, p22, ...  p2j}}
{"Cactuses",  {p31, p32, ...  p3j}}
pXj est le nombre d'entités de la série tel que calculé lors de la jième mise à jour (nombre de scorpions pour X valant 1, de lézards pour X valant 2 etc.)

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

{new_data = { "Scorpions", p1j+1}, {"Lizards", p2j+1}, {"Cactuses", p4j+1}}
où chacun des pXj+1 est le nombre d'éléments de chaque type (scorpions, lézards etc.) tel qu'on peut le calculer à l'instant courant à partir du Environment.

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

{"Scorpions", {p11, p12, ...  p1j+1}}
{"Lizards",  {p21, p22, ...  p2j+1}}
{"Cactuses",  {p31, p32, ...  p3j+1}}

Le pas 2 de l'algorithme peut être implémenté très simplement au moyen de l'appel updateData(deltaEpoch, new_data) sur le graphe à mettre à jour. deltaEpoch 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 Environment::fetchData prenant en paramètre le titre du graphe (une string) et retournant l'ensemble new_data lui correspondant.

Par exemple :

[Question Q5.2] 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? Répondez à cette question dans votre fichier REPONSES et réalisez les implémentations nécessaires.

Test 30  : Affichage de courbes d'évolution

Lancez votre simulation comme précédemment. Créez tout à coup plusieurs cactus, scorpions et lézards pour constater que les courbes associées "réagissent" en conséquence (pics de croissance). Au final, vous devriez pouvoir observer les courbes d'évolution conjointes, comme dans notre exemple du départ.

Modele

Le baculement vers la vue "modèle neuronal» devrait automatiquement permettre le basculement vers les statistiques de décomptes des ondes que vous devriez voir s'afficher de façon appropriées:

Modele
Notez que vous pouvez agrandrir la hauteur de la fenêtre d'affichage des statistiques dans les fichiers .json:
    "window":{
      "antialiasing level":4,
      "title":"POOSV Simulation",
       "simulation" : { "size" : 600 },
      "stats":{
         "height":200 <-ICI
      },
       "control" : { "width" : 200 }  
    
Libre à vous ensuite de faire afficher toute statistique qui vous semblerait pertinente moyennant le même graphe ou de nouveaux graphes ( nombre d'animaux mâles et femelles, taux de reproduction etc.

Notez que l'on peut adapter l'échelle d'affichage des graphes au moment où ils sont ajoutés (voir la méthode FinalApplication::resetStats). Par exemple, si vous simulez régulièrement des grandes populations:

addGraph(s::GENERAL, { s::SCORPIONS, s::LIZARDS, s::CACTUSES,}, 0, 500);
 
sera plus adapté que :
addGraph(s::GENERAL, { s::SCORPIONS, s::LIZARDS, s::CACTUSES,}, 0, 200);
 
Vous pouvez enfin si vous le souhaitez activer les statistiques dans les autres tests graphiques en vous inspirant du matériel existant dans FinalApplication.hpp/.cpp.

Statistiques spécialisées (bonus)

Vous pouvez compléter votre programme de sorte à ce que :

[Question Q5.3] Quelles modifications apportez-vous et dans quelles classes pour ajouter les traitements souhaités ? Répondez à cette question dans votre fichier REPONSES et réalisez les implémentations nécessaires.

Test 31  : Statistiques spécifiques (bonus)

Lancez votre simulation comme précédemment. Basculez sur la simulation «modèle neuronal» (au moyen de la touche Tab). Créez des NeuronalScorpion et des WaveLizard. Basculez à nouveau sur la simulation à l'échelle des populations et vérifiez que le décompte des scorpions et lézards y reste nul.

Vous voici arrivés au bout de la partie obligatoire du projet. Bravo pour tout le travail!
L'étape suivante est facultative. Elle vous permet de gagner des points bonus mais surtout de donner libre cours à votre imagination pour étendre ce projet :-)

Retour à l'énoncé du projet (partie 5) Module suivant (partie 6)