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
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 :
=================================================================== 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 :Comment éviter les inclusions multiples :
#pragma once
Convention de nommage pour les fichiers :
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 :
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 :
#include <Utility/Vec2d.hpp>
Vous doterez la classe CircularBody des méthodes suivantes :
un constructeur initialisant la position et le rayon de l'objet circulaire au moyen d'un Vec2d et d'un double passés en paramètres (dans cet ordre). Vous ne ferez pour le moment aucun test sur la validité des valeurs passées en paramètres.
des «getters» (getCenter et getRadius) pour la position du centre et le rayon.
une méthode isColliding prenant en argument un CircularBody other, et retournant true si other est en collision avec l'instance courante et false sinon. Soient deux CircularBody a et b, a est en collision avec b si la distance entre les centres de a et b est inférieure ou égale à la somme des rayons de a et b;
une méthode isPointInside prenant en argument un point (de type Vec2d) et retournant true si le point est à l'intérieur de l'instance courante et false sinon. Un point p est à l'intérieur d'un CircularBody body si la distance entre p et body est inférieure ou égale au rayon de body;
l'opérateur | utilisable comme suit :
body1 | body2retournant true si body2 est en collision avec body1 et false sinon. Les objets body1 et body2 sont de type CircularBody;
l'opérateur > utilisable comme suit :
body > pointretournant true si point est à l'intérieur de body et false sinon. Les objets body est de type CircularBody et point est de type Vec2d;
[Question Q1.1] Comment coder les deux opérateurs précédemment décrits en évitant toute duplication de code dans le fichier CircularBody.cpp ? Réfléchissez et répondez à cette question dans votre fichier REPONSES.
[Question Q1.2] Quelle surcharge choisissez-vous pour les opérateurs qu'il vous a été demandé de coder jusqu'ici (interne ou externe) et comment justifiez-vous ce choix ? Réfléchissez et répondez à cette question dans votre fichier REPONSES.
[Question Q1.3] 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.4] 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.
Pour tester cette partie du projet vous disposez :
de la cible circularBodyTest définie dans le fichier partie1/src/CMakeLists.txt et qui fait appel à plusieurs fichiers;
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(...)
=============================================================================== All tests passed (60 assertions in 9 test cases)(et qui montre que la classe Vec2d que nous avons fournie passe avec succès un certain nombre de tests)
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.
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 :
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)'
Le lancement de la cible circularBodyTest devrait donc produire la fin de sortie suivante :
=============================================================================== All tests passed (36 assertions in 1 test case)
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.
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 :
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;
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 :
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 :
Complétez votre classe Substance en y ajoutant :
Que faire si les valeurs passées en argument au constructeur dépassent les seuils limites ?
Un choix simple ici consiste à faire plafonner les valeurs: si une valeur est inférieure à SUBSTANCE_PRECISION elle devient nulle. Si elle est positive mais dépasse le maximum, on la ramènera au maximum. Vous pourrez utiliser les fonctions min et max de la bibliothèque <algorithm>
Substance substance(255.0, 432.5, 214.3); cout << substance[VGEF] << endl; // affiche 255.0 cout << substance[GLUCOSE] << endl; // affiche 432.5 cout << substance[BROMOPYRUVATE] << endl; // affiche 214.3
[VGEF] : 200.0 [GLUCOSE] : 550.5 [BROMOPYRUVATE] : 120.0
Substance subs1(300.0, 400.0, 200.0); Substance subs2(200.0, 100.0, 200.0); subs1 += subs2; cout << subs1 << endl;devrait afficher:
[VGEF] : 500.0 [GLUCOSE] : 500. [BROMOPYRUVATE] : 400.0
Substance subs1(300.0, 400.0, 100.0); Substance subs2(200.0, 100.0, 200.0); subs1 -= subs2; cout << subs1 << endl;devrait afficher:
[VGEF] : 100.0 [GLUCOSE] : 300. [BROMOPYRUVATE] : 0.0
Exemple:
Substance subs1(300.0, 400.0, 100.0); Substance subs2 = subs1 * 2.0; cout << subs2 << endl;devrait afficher :
[VGEF] : 600.0 [GLUCOSE] : 800. [BROMOPYRUVATE] : 200.0
Substance subs1(100., 200., 300.); Substance subs2(300., 200., 100.); subs1.uptakeOnGradient(0.5, subs2, GLUCOSE); cout << subs1 << endl; cout << subs2 << endl;devrait afficher
[VGEF] = 100 [GLUCOSE] = 100 [BROMOPYRUVATE] = 300 [VGEF] = 300 [GLUCOSE] = 300 [BROMOPYRUVATE] = 100
[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.
Pour tester la classe Substance vous disposez :
de la cible substanceTest définie dans le fichier partie1/src/CMakeLists.txt;
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.
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)