Projet : étape 2.2
Nutriments

Buts: Permettre à des sources de nutriments de croître spontanément dans la boite de culture, en fonction de la température.

Paramétrages de la simulation

Avant de nous pencher plus en détail sur la modélisation des nutriments, 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, pour les nutriments: quelle représentation graphique choisir, quelle quantité de nutriment maximale et minimale peut-on créer, dans quelles conditions de température la source de nutriment peut croître dans la boite de culture 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 d'expérimentation 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és 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.

Le fichier de configuration associée à cette étape du projet se trouve 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.

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, les paramètres liés aux nutriments sont exprimés sous cette forme

"nutrients" : {
      "growth" : {
            "speed" : 5,
             "max temperature" : 60,
             "min temperature" : 30
            },
       "quantity" : {
             "max" : 50,
             "min" : 40
            },
       "texture" : "foodA.png"
}
ce qui se lit comme :

La fonction getAppConfig() de Application.hpp permet d'accéder à la valeur d'un paramètre donné. Il faudra étiqueter la valeur cherchée par toutes les chaînes de caractères le décrivant, au moyen d'opérateur d'indexation .

Par exemple, pour accéder à la température maximale permettant la croissance d'une source de nutriments, on écrira :

getAppConfig()["nutrients"]["growth"]["max temperature"]

La valeur obtenue doit enfin être convertie dans le type attendu au moyen de méthodes de conversion fournies (toDouble() pour les doubles, toString() pour les string, toBool pour les booléens etc.) Par exemple:

getAppConfig()["nutrients"]["texture"].toString()
(car la texture est censée être un string) ou
getAppConfig()["nutrients"]["growth"]["max temperature"].toDouble()
(car la température est censée être un double)
Notez enfin que si l'on ne spécifie pas toutes les chaînes de caractères descriptives, on obtient l'ensemble des valeurs non spécifiées sous la forme d'un tableau. Par exemple :
getAppConfig()["nutrients"]["growth"]
retourne un tableau contenant les valeurs associée à "speed", "max temperature" et "min temperature" ({5, 60, 30} dans le fichier app.json fourni)

Notez aussi que pour raccourcir l'écriture de l'accès aux paramètres, il est possible de définir des constantes qui y sont relatives dans le fichier Config.hpp.
Par exemple, un raccourci a été créé pour le paramètre getAppConfig()["simulation"]["time"]["factor"] qui peut s'écrire getShortConfig().simulation_time_factor" (regardez comme cela et codé dans Config.[hpp][cpp]).

Attention ces raccourcis n'ont été créés que pour quelques paramètres, libre à vous d'en rajouter par la suite dans Config.[hpp][cpp]).

Pourquoi pas de simples constantes ?

L'inconvénient d'utiliser de simples constantes globales est qu'à chaque fois que l'on veut modifier leur valeur, il faut recompiler et relancer le programme. Pour contourner cette limitation, le noyau de simulation fourni (Application) est doté d'un attribut de type Config et des méthodes getAppConfig() et getShortConfig() qui permettent d'aller puiser des valeurs dans les fichiers de configuration JSON. Pour changer une valeur en cours de simulation, sans avoir à recompiler le code, il suffit d'éditer le fichier JSON et de le recharger au moyen de la touche 'C' ('C' pour "Configuration").
Chaque test graphique que vous lancerez pourra être paramétré par le fichier de configuration souhaité, par exemple, en ligne de commande:
       ./nutrientTest appTest.json
     
permettra de lancer le programme application en utilisant un fichier appTest.json comme fichier de configuration. Si l'on ne met rien, il s'agira du fichier dont le nom correspond à la constante DEFAULT_CFG définie dans Config.hpp. Dans le matériel fourni, il s'agit de app.json, mais libre à vous de le modifier.

Nous pouvons maintenant retourner au centre de nos préoccupations.

La classe Nutrient

Il vous est maintenant demandé de compléter la classe Nutrient qui va servir à modéliser les sources de nutriments consommables par les bactéries. Pour simplifier nous supposerons pour commencer qu'elles mangent toutes la même chose !

[Question Q2.6] Comme quasiment toutes les entités à modéliser, nos sources de nutriments seront aussi des contours circulaires. Comment proposez-vous d'utiliser la classe fournie CircularBoundary (dans le répertoire Lab/) pour modéliser cet aspect ? Répondez à cette question dans votre fichier REPONSES.

La classe Nutrient sera spécifiquement caractérisée par une quantité (de nutriments disponible). Vous partirez de l'hypothèse qu'une source de nutriment a connaissance de la boite à laquelle elle appartient (pour simplifier, de son indice dans l'ensemble des boites du laboratoire).

Le fichier fourni Utility/Types.hpp contient la définition d'un certain nombre de types prédéfinis, dont le type Quantity dont vous pouvez tirer parti ici.

[Question Q2.7] A quoi cela peut-il bien servir d'utiliser le type Quantity plutôt que tout simplement un double ? Répondez à cette question dans votre fichier REPONSES.

Vous doterez la classe Nutrient :

Libre à vous d'ajouter d'autres méthodes plus tard si vous l'estimez nécessaire.

Dessin d'un Nutrient

Un certain nombre de textures sont fournies dans le répertoire res.

La méthode Application::getAppTexture permet de charger une texture de ce répertoire dans un objet de type sf::Texture (à propos du texturage vous pouvez revoir l'exemple du tutoriel).

Par ailleurs, le fichier fourni Utility.[hpp][cpp] vous offre une méthode buildSprite qui vous permet de construire un objet texturé à afficher.

Le code pour l'affichage d'une source de nutriments devrait donc ressembler à ceci :

      auto nutrientSprite = buildSprite(centre, une_taille_graphique, texture));
      target.draw(nutrientSprite);

Affichages en mode "debugging"

Certains affichages vous seront demandés en plus dans le projet, afin de faciliter certains tests. Votre programme devrait donc pouvoir fonctionner en mode "debugging" ou pas.

Le fichier de configuration contient un paramètre "debug" que vous pouvez utiliser à cet effet (et le fichier fourni Application offre la fonction isDebugOn()).

Il vous est donc demandé de compléter l'affichage de la source de nutriments de sorte que si le mode debugging est activé (paramètre associé à l'étiquette "debug" valant true), la quantité de nutriments associée à la source s'affiche aussi.

Pour afficher du texte, vous pouvez utiliser la fonction sf::Text buildText fournie dans Utility.[hpp][cpp] .

Ajout de nutriments à la boite de culture

Pour voir vos méthodes d'affichage à l'oeuvre, il ne reste plus qu'à permettre l'ajout de nutriments à la boite de culture.

Pour ce faire, complétez la méthode CultureDish::addNutrient de sorte à ce qu'elle ajoute un Nutrient* à la collection de sources de nutriments de la boite de culture. Ce dernier ne sera ajouté que si sa position lui permet de se trouver à l'intérieur de la boite (son contour circulaire est contenu dans celui de la boite). Dans ce cas, la méthode addNutrient retournera true pour indiquer que le nutriment a bien trouvé sa place dans la boite. Dans le cas contraire, aucun nutriment ne sera ajouté et addNutrient retournera false. Pour effectuer les tests nécessaires, vous utiliserez les méthodes programmées dans CircularBoundary en veillant à une bonne modularisation de votre code!

[Question Q2.8] En examinant le code du test fourni pour cette partie (src/Tests/GraphicalTests/NutrientTest.cc) quelle méthode doit-elle être ajoutée à la classe Laboratory pour permettre l'ajout de nutriments à la boite de culture courante lorsque l'on appuie sur la touche 'N', une fois la ligne en question décommentée  ? Quelle méthode existante doit être modifiée pour permettre le dessin des sources de nutriments nouvellement ajoutées? Répondez à ces question dans votre fichier REPONSES puis programmez cette méthode.

Test 3  : Nutriments (affichage et consommation)

Pour tester vos affichages, décommentez dans le ficher tests NutrientTest les lignes de code correspondant à la programmation des touches 'N' et 'T'.

Lancez alors le test comme précédemment en ayant pris soin de mettre l'application en mode «debugging». Ce mode peut-être activé/désactivé au moyen de la touche 'D'.

En plaçant votre souris sur une position de l'environnement et en cliquant sur 'N' vous devriez y voir apparaître une source de nourriture.

La touche 'T' permet de prélever de la dernière source de nutriments créée une quantité de 15. En appuyant successivement sur 'T' vous devriez voir la quantité de nutriments décroître jusqu'à atteindre 0. Appuyer sur la touche 'T' doit alors rester sans effet.

Vous prendrez soin de vérifier que les tentatives d'ajout de nutriments trop proches de la bordure de la boite ou en dehors de celle-ci ne produisent aucun effet.

Vérifiez aussi que les quantités ne s'affichent pas textuellement lorsque le mode «debugging» est désactivé.

Un exemple de déroulement de test est fourni dans la petite vidéo suivante:
← en appuyant sur 'T' la dernière source de nutriments ajoutée est décrémentée de 15 jusqu'à atteindre la valeur 0.

Notez que pour afficher en plus gros, il vous suffit de modifier la taille du "monde" ("size") dans le fichier app.json
  "simulation" : {
        ....
        "background" : "sand.png",
	"debug background" : "sand.png",
        "size" : 800
    }

Croissance des nutriments en fonction de la température

Nous avons jusqu'ici fait appel aux méthodes drawOn pour afficher graphiquement les éléments de notre simulation qui demeurent, pour l'heure, figés et statiques.

Nous allons nous intéresser maintenant à la facette update pour commencer à insuffler une peu de dynamique à notre simulation.

Commencez compléter dans votre classe Nutrient, la méthode void update(sf::Time dt) permettant de calculer l'évolution du nutriment après écoulement d'un pas de temps dt.

L'évolution consistera à le faire croître d'une quantité:

auto growth = speed * dt.asSeconds();

speed est la valeur double du paramètres liés aux étiquettes ["nutrients"]["growth"]["speed"] dans le fichier de configuration.

Les contraintes liées à la croissance sont les suivantes :

  1. La croissance ne sera possible que si la température de sa boite de culture se trouve entre des bornes minTemp et maxTemp, où minTemp et maxTemp sont les valeurs associées au paramètres étiquetés respectivement ["nutrients"]["growth"]["min temperature"] et ["nutrients"]["growth"]["max temperature"].
  2. La quantité ne peut augmenter au delà du double de la valeur associée au paramètre étiqueté ["nutrients"]["quantity"]["max"]
  3. La source de nutriment ne peut déborder de la boite (ce qui est le cas si son cercle englobant n'est pas entièrement contenu dans celui de la boite).

[Question Q2.9] Sachant que la classe Laboratory ne donne pas d'accès à ses CultureDish via des getters. Comment permettre à Nutrient::update de connaître la température de sa propre boite de culture (ou d'en connaitre les limites pour ne pas en déborder) en utilisant Application::getAppEnv,  ? Répondez à cette question dans le fichier REPONSES et programmez ce qui est nécessaire.

Contrôle de la température

Tout comme pour la sélection de la boite courante, la température de la boite courante est un paramètre contrôlable directement depuis l'interface graphique. Faites en sorte que la valeur ajoutée à la température ou qui en est supprimée soit celle associée à l'étiquette ["culture dish"]["temperature"]["delta"] et faites en sorte que la température se situe entre les deux bornes données par les étiquettes ["culture dish"]["temperature"]["min"] et ["culture dish"]["temperature"]["max"]. Vérifiez ensuite que vos méthodes Lab::increaseTemperature et Lab::decreaseTemperature sont correctement codées lorsque vous sélectionnez la température comme paramètre à contrôler. Vérifiez aussi que les températures des différentes boites peuvent chacune être contrôlées de manière indépendante (chaque boite peut avoir sa propre température).

Complétez enfin la méthode Lab:resetControls de sorte à de qu'elle permette de ramener la température de la boite courante à la température par défaut (celle donnée à la construction de la boite). La méthode reset devra aussi provoquer cet effet (sans duplication de code!).

Test 4  : Nutriments (croissance et boites multiples)

Pour tester vos derniers développements, lancez le test comme précédemment et créez des sources de nutriments en appuyant sur la touche 'N'. Ils devraient toujours être figés ! La raison est que les nutriments ne croissent qu'à partir de la valeur 30 pour la température (regardez le fichier app.json):

 "nutrients" : {
	"growth" : {
            "speed" : 5,
            "max temperature" : 60,
            "min temperature" : 30
 },
  
Augmentez la température avec les contrôles que vous avez programmé, vous devriez voir les sources de nourriture commencer à croître à partir de la valeur 30.

← La température est augmentée dans la première boite, puis on passe à la seconde où on augmente la température puis on la diminue pour figer la croissance. Dans la troisième boite, on laisse la température stable. En naviguant d'une boite à l'autre on voit des contenus et des températures différents.
[Video : Croissance des nutriments en fonction de la température
& cultures dans plusiseurs boites]
Jouez avec les touches du menu pour vérifier que la croissance est bien stoppée lorsque les conditions de température ne sont pas vérifiées. Vérifiez aussi visuellement que les limites de croissance imposées sont bien vérifiées : une source de nutriments ne peut déborder de la boite et ne croît pas au delà du double de la quantité maximale liée à ["nutrients"]["quantity"]["max"] (fixée à 50 dans le fichier de paramètres fourni).

Vérifiez que la touche 'R' a bien pour effet de vider la boite courante et de réinitialiser sa température à la valeur par défaut.

Vérifiez enfin que chaque boite peut avoir sa propre composition et sa propre température et que de passer de l'une à l'autre préserve ces contenus.

Pour comprendre enfin les effets de la touche 'C', alors que votre programme est encore ouvert, changez la valeur de ["simulation"]["time"]["factor"] dans le fichier de configuration (par exemple faites la passer à 5). Appuyer sur 'C' pour recharger cette nouvelle valeur dans la simulation. Si vous créez des nutriments avec les bonnes conditions de température vous devriez les voir croître beaucoup plus vite. Tous les paramètres de simulation peuvent ainsi être modifiés en cours de d'exécution du programme, sans avoir à relancer le programme.

Petite retouche au code de CircularBoundary

Il est souvent nécessaire en programmation de remettre en question des choix antérieurs et d'apporter des modifications utiles au code existant ("code refactoring"). C'est d'autant plus nécessaire dans le cadre de ce projet, que vous ne connaissez pas forcément toutes les notions utiles au moment d'aborder telle ou telle partie.

Si vous avez correctement fait les choses la semaine passée, votre classe CircularBoundary a été programmée en déclarant comme privés les attributs et comme publiques les méthodes.

Pour améliorer cette conception, apportez les modifications garantissant que:

ne soient possible que dans ses sous-classes (et n'oubliez pas de vérifier que votre programme fonctionne toujours, une fois ces modifications faites ;-))
Retour à l'énoncé du projet (partie 2)


Last modified: Fri March 4 16:31:50 CEST 2026