Projet : étape 1
"Classes utilitaires"

But: Cette première étape a pour but de vous faire programmer deux classes utilitaires :

Concepts nécessaires: classes d'objets, constructeurs/destructeurs, surcharge d'opérateurs.

Ces concepts sont expliqués dans les cours 15 à 18. Les exercices de la série 19 contiennent du matériel proche de ce que vous devez programmer.

Fichiers utiles: partie1.zip


Mise en place

Les indications ci-dessous présupposent que vous avez créé un dossier particulier pour le projet (nous supposerons ici qu'il s'agit d'un répertoire cpp/projet). Adaptez les commandes à l'endroit où se trouve le projet chez vous.

Copiez l'archive fournie partie1.zip dans votre répertoire cpp/projet et décompressez-la :

Dans QtCreator, configurez ensuite le projet en suivant scrupuleusement les instructions données ici (il s'agit d'ouvrir le fichier CMakeLists.txt du répertoire src/ et des instructions qui suivent). La seule différence avec la marche à suivre est que vous allez utiliser partie1.zip et partie1/ à la place de sfml_with_cmake.zip comme nom d'archive et de sfmlCmake/ comme nom de dossier. Une fois le projet configuré, vous devriez pouvoir le compiler et l'exécuter. L'exécution doit produire la sortie suivante dans la fenêtre «Application output»:
	===================================================================
	All tests passed (60 assertions in 9 test cases)
      
La signification de cette exécution vous sera expliquée un peu plus bas.

L'archive fournie contient :

Pour augmenter la modularité de votre projet, le prototypage des méthodes et la déclaration des attributs devront se trouver dans un fichier avec l'extension .hpp tandis que leurs définitions se trouveront dans un fichier avec l'extension .cpp.

À propos de la compilation, quelques indications utiles :
  1. Comment éviter les inclusions multiples :

    Les classes que vous allez programmer dans le projet vont se combiner de façon relativement complexe et vous allez parfois devoir inclure de nombreux fichiers .hpp existants au début d'un nouveau fichier .hpp. Si des fichiers ont été inclus de façon redondante dans ces différents .hpp, le compilateur réagira par un message d'erreur. Pour éviter cette situation, il est nécessaire de mettre au début de chaque fichier monfichier.hpp la directive :
    #pragma once
    
  2. Convention de nommage pour les fichiers :

    Les fichiers porteront les mêmes noms que les classes qu'ils contiennent : par exemple, les fichiers Animal.[hpp][cpp] contiendront le code relatif à la classe Animal. Il ne s'agit bien sûr que d'une convention.
    Certains systèmes d'exploitation comme Windows sont insensibles à la casse (c'est le cas aussi des VMs de l'EPFL)  : Vec2d.hpp sera donc interprété de la même manière que vec2d.hpp (commençant par un petit "v"). Inclure le fichier vec2d.hpp sera donc interprété comme correct par le compilateur, même si votre fichier s'appelle Vec2d.hpp. Ce n'est pas le cas dans un environnement Unix classique. Faites donc attention à la cohérence car votre projet doit pouvoir s'exécuter partout.
  3. Compilation dans un terminal (Linux, MacOs):
    Le matériel fourni est intégrable à QtCreator dans lequel vous pouvez compiler et exécuter. Sous Linux et MacOs, vous pouvez aussi parfaitement l'utiliser en ligne de commande comme indiqué ici.

Modules à programmer

La classe CircularBody

Au cours de leurs déambulations, les hamsters pourront potentiellement croiser des objets d'intérêt comme des sources de nourriture ou encore des obstacles entravant leur liberté de se déplacer. Votre programme devra donc être capable de tester, de façon simple, si deux objets sont en collision. Tester la collision entre deux objets peut s'avérer très complexe selon leurs formes effectives. Nous allons donc, pour simplifier, assimiler les objets à des formes englobantes plus simples lorsqu'il s'agit de gérer les collisions.

La classe CircularBody qu'il vous est suggéré d'écrire ici modélise la représentation géométrique simplifiée que vont prendre les objets simulés pour réaliser les tests de collision. Cette forme est simplement un cercle englobant.

En clair, certaines entités de la vue externes (hamster, tas de nourriture etc.) seront perçus comme de simples cercles lorsqu'il s'agira de tester s'ils se rencontrent ou pas :

item il y a « rencontre » entre un hamster et un tas de nourriture si les cercles englobant (en grisé) auxquels ils sont associés se croisent.

Un objet de type CircularBody est caractérisé par  :

Pour pouvoir utiliser le type Vec2d, l'inclusion suivante est nécessaires :
#include <Utility/Vec2d.hpp>

Méthodes

Vous doterez la classe CircularBody des méthodes suivantes :

Programmes de test

Pour tester cette partie du projet vous disposez :

  1. de la cible circularBodyTest définie dans le fichier partie1/src/CMakeLists.txt et qui fait appel à plusieurs fichiers;

  2. cette cible permet de lancer le programme de test src/Tests/UnitTests/CircularBodyTest.cpp. Ce programme fait appel aux différentes fonctionnalités que vous avez codées dans la classe CircularBody pour contrôler qu'elles sont correctes.

Jetez petit coup d'oeil au fichier CMakeLists.txt pour voir comment est définie cette cible:

#       add_executable (circularBodyTest  ${TEST_DIR}/UnitTests/CircularBodyTest.cpp ${SOURCES} ${CACHE_SOURCES})
#	target_link_libraries(...)

Afin de comprendre le principe de fonctionnement de ces tests, ouvrez le fichier src/tests/UnitTests/CircularBodyTest.cpp pour l'examiner.

Ce programme de test utilise un framework particulier, appelé Catch, permettant de faire ce que l'on appelle dans le jargon de la programmation des tests unitaires. Il s'agit en fait d'écrire des scénarios de tests pour différentes fonctionnalités codées.

Nous vous expliquons ci-dessous le principe de fonctionnement de ces tests.

Principe de fonctionnement des tests unitaires (non graphiques)

Le programme de test fourni dans CircularBodyTest.cpp définit des scénarios (« test case ») permettant de tester les fonctionnalités de la classe CircularBody en faisant appel aux méthodes que l'on vous a demandé d'y coder.

Chaque scénario contient un certain nombre de messages (étiquetés par les mot-clés GIVEN, THEN ou AND_THEN) qui vont décrire les conditions de déroulement des différents tests effectués et leur résultats. Ces messages ne s'afficheront qu'en cas d'échec d'un test.

Si tous les tests d'un scénario se déroulent correctement, vous verrez simplement s'afficher un message ressemblant à ceci :

===============================================================================
All tests passed 

Les tests consistent à vérifier un certain nombre d'assertions exprimées au moyen de fonctionnalités telles que CHECK, CHECK_FALSE, CHECK_APPROX_EQUAL .

Le test fourni ne contient qu'un seul scénario (depuis la ligne 11 jusqu'à le fin), mais il peut y en avoir plusieurs en général. Examinons une partie de ce scénario.

Les lignes 15 et 16 construisent deux CircularBody identiques en position {1,1} et de rayon 2. Les deux objets ainsi construits sont censés être en collision et c'est ce qui est indiqué dans le THEN en ligne 24. Pour tester que votre code fonctionne correctement pour ce cas, le test demande de vérifier 6 assertions :

  1. que le centre des objets est conforme à ce qui est attendu;
  2. qu'il en va de même pour le rayon;
  3. que le premier objet est en collision avec le second (selon les retours de votre méthode isColliding et de votre opérateur operator|);
  4. et que le second objet est en collision avec le premier (selon des modalités identiques).

Vous l'aurez compris, un CHECK réussit si l'assertion en paramètre retourne true. De même un CHECK_FALSE (par exemple en ligne 82) réussit si la condition qui lui est passée en argument retourne false.

Supposons que votre méthode isColliding soit mal codée, le lancement du programme de test CircularBodyTest.cpp affichera :

-------------------------------------------------------------------------------
Scenario: Collision
     Given: Two identical circular bodies
      Then: they collide
-------------------------------------------------------------------------------
...
src/Tests/UnitTests/CircularTest.cpp:26: FAILED:
CHECK( o1.isColliding(o2) )
with expansion:
  false                    
...............................................................................

 

Le test affiche donc le nom associé au scénario, les messages associés à GIVEN et THEN pour indiquer ce que l'on a en entrée (deux CircularBody identiques) et ce qui est supposé être vrai dans ce cas ("...they collide") puis la liste des assertions échouées et leur lignes. La partie with expansion indique que l'on a essayé de prouver qu'une assertion était vraie alors que ce qui a été trouvé ("with expansion") est false.

Le lancement de la cible circularBodyTest devrait, une fois le programme corrigé, produire la fin de sortie suivante :

using ./../res/app.json for configuration.

===============================================================================
All tests passed (36 assertions in 1 test case)

  

Note: dans QtCreator cette sortie est visible dans la fenêtre «Application output»

Ceci indique que 36 assertions (CHECK, CHECK_FALSE etc.) ont été lancées avec succès dans le cadre d'un scénario unique ("1 test case").

Notez que dans le cas où les fonctionnalités testées ne sont pas présentes dans votre code, le lancement du test provoquera des erreurs de compilation.

Par exemple, supposez que le constructeur n'est pas codé chez vous conformément à ce qui a été demandé (arguments manquants ou dans le désordre), vous aurez alors un message d'erreur tel que :

error: no matching function for call to 'CircularBody::CircularBody(const Vec2d&, double)'
Si vous voulez tester les méthodes au fur et à mesure que vous les codez, ce qui est recommandé, il vous suffit de commenter les CHECK ou CHECK_FALSE qui font appel à des méthodes non encore codées.

Test 1 : Fonctionnalités de CircularBody

Les tests sont numérotés globalement dans tout le projet. Il s'agit ici du premier test du projet. Chaque test est un élément noté.

Le lancement de la cible circularBodyTest devrait donc produire la fin de sortie suivante :

===============================================================================
All tests passed (36 assertions in 1 test case)


La classe Substance

Paramètres de simulation

Avant de nous pencher sur le codage de la classe suivante, un petit détour sur la question de comment représenter les paramètres caractéristiques d'une simulation s'impose.

Il peut en effet y en avoir de très nombreux. Par exemple, quelles dimensions donner aux différents composants graphiques, quelle représentation graphique utiliser pour dessiner les hamsters, etc.

Il est évidemment souhaitable que les valeurs de ces paramètres fassent partie d'un fichier de configuration (plutôt que de les coder « en dur » dans le programme). Ceci nous permettra de faire varier à souhait les exécutions en modifiant simplement le fichier de configuration et donc d'expérimenter facilement de nombreuses situations différentes.

Il existe plusieurs formats standard pour la modélisation de données (XML, JSON, YAML etc), qui d'ailleurs ne sont pas uniquement utilisé dans le contexte de la programmation.

Pour ce projet, nous avons pris le parti d'utiliser le format JSON pour répertorier les paramètres caractéristiques de la simulation.

Les fichiers de configuration du projet sont les fichiers res/appX.json.

Chaque programme que vous lancerez pourra être paramétré par le fichier de configuration comme nous aurons l'occasion de le voir un peu plus tard.

Le fichier de configuration par défaut est res/app.json.

Nous vous fournissons dans le répertoire src/JSON toutes les fonctionnalités nécessaires à la récupération des données depuis un tel fichier.

Dans les fichiers .json, les paramètres sont étiquetés de manière hiérarchique au moyen de chaîne de caractères qui permettent de les décrire.

Par exemple :

{
    "simulation" : {
       ...
        "substance" : {
             "max value" : 200000

           },
        },
      ...
}
se lit comme :
« une substance ("substance") de la simulation ("simulation" ) a pour paramètre configurable sa concentration maximale ("max value") etc. »

La classe utilitaire fournie Config permet d'accéder à la valeur d'un paramètre donné (en l'associant à une constante publiquement accessible). Par exemple la constante substance_max_value permettra d'accéder à la valeur du paramètres étiqueté ["simulation"]["substance"]["max value"] dans le fichier .json.

La classe Application dispose d'un attribut de type Config, qu'elle d'initialise au moyen du bon fichier .json, et c'est par ce biais que vous pourrez accéder aux paramètres par une tournure de ce type :

getAppConfig().substance_max_value;
Les tournures faisant appel à getAppConfig() (dont vous expérimenterez plus tard qu'elles ont très pratiques pour changer dynamiquement les paramètres de simulation sans recompiler le code), ne doivent pas être utilisées pour initialiser des constantes globales: elles font appel à un fichier de configuration stocké dans un objet de type Application créé au lancement des tests et il n'est pas garanti que cet objet soit créé au moment où les constantes globales sont évaluées. Il est déconseillé par ailleurs de faire proliférer dans votre code des constantes globales. Quelques constantes utiles sont néanmoins à votre disposition dans Utility/Constants.hpp.

La classe Substance

Nous avons vu dans la description générale du projet, qu'au niveau interne  :

Plus précisément, une case va représenter une portion de ce que l'on appellera la matrice extra-cellulaire (ECM).Cette matrice servira de support aux cellules de l'organe simulé et au système sanguin les irriguant.

Dans notre modèle, c'est par le biais de l'ECM que vont transiter les substances influant sur les différents composants du système :

  1. le réseau sanguin va diffuser le glucose et les inhibiteurs sur l'ECM;
  2. ces substances seront absorbées depuis l'ECM par les cellules du foie pour leur permettre de produire de l'énergie (avec plus ou moins de succès selon la quantité d'inhibiteurs);
  3. les cellules tumorales vont également diffuser une substance, le VGEF, favorisant la croissance du réseau sanguin via l'ECM;
  4. cette substance sera récupérée par le système sanguin qui va s'en "nourrir" pour se développer etc..

Toute cette dynamique sera simulée case par case au niveau interne.

Il vous est donc demandé à cette étape du projet de coder une classe Substance représentant de façon synthétique l'ensemble des substances présentes à un moment donné sur une case de l'ECM.

Cette classe sera utilisée par la suite pour mettre en oeuvre les différents phénomènes liés à la diffusion ou l'absorption de glucose, d'inhibiteur et de VGEF.

Une instance de la classe Substance représente donc l'ensemble des substances présentes à un moment donné sur une case de l'ECM.

Elle sera caractérisée par :

Les trois dernières valeurs seront aussi représentées par des double.

Complétez votre classe Substance en y ajoutant :

[Question Q1.5] Quels arguments de méthodes, parmi celles qu'il vous a été demandé de coder ci-dessus, vous semblent-ils judicieux de passer par référence constante ? Répondez à cette question dans votre fichier REPONSES.

[Question Q1.6] Quelles méthodes, parmi celles qu'il vous a été demandé de coder ci-dessus, vous semblent-ils judicieux de déclarer comme const ? Répondez à cette question dans votre fichier REPONSES.

Test 2 : Fonctionnalités de Substance

Pour tester la classe Substance vous disposez :

  1. de la cible substanceTest définie dans le fichier partie1/src/CMakeLists.txt;

  2. cette cible permet de lancer le programme de test src/Tests/UnitTests/SubstanceTest.cpp. Ce programme fait appel aux différentes fonctionnalités que vous avez codées dans la classe Substance pour contrôler qu'elles sont correctes.

Décommentez cette cible dans le fichier CMakeLists.txt (enlever les `#` au début des lignes 85 et 86 ainsi que 118 et 119) lorsque vous serez prêts à tester votre classe Substance. N'oubliez pas la commande Build > Run CMake dès que vous apportez un changement au fichier CMakeLists.txt

Le lancement de la cible substanceTest devrait produire une sortie ressemblant à ceci :

Using ./../res/app.json for configuration.
[VGEF] = 0
[GLUCOSE] = 300
[BROMOPYRUVATE] = 400

[VGEF] = 300
[GLUCOSE] = 200
[BROMOPYRUVATE] = 100

[VGEF] = 45
[GLUCOSE] = 675
[BROMOPYRUVATE] = 43

===============================================================================
All tests passed (77 assertions in 6 test cases)

  
En plus des 77 assertions correctes, l'affichage des substances devra être identique (du point de vue du contenu) à celui de cette trace d'exécution.

Retour au document principal