Project : step 2.2
Nutrients

Disclaimer: the automatically generated English translation is provided only for convenience and it may contain wording flaws. The original French document must be taken as reference!

Goals: Allow nutrient sources to grow spontaneously in the culture box, depending on the temperature.

Simulation settings

Before we look in more detail at nutrient modelling, it is worth taking a brief detour to consider the question of how to represent the characteristic parameters of a simulation.

There can indeed be a great many of them. For example, regarding nutrients: which graphical representation to choose, what maximum and minimum nutrient levels can be set, under what temperature conditions the nutrient source can grow in the culture box, and so on.

It is obviously preferable for the values of these parameters to be stored in a configuration file (rather than hard-coding them directly into the programme). This will allow us to vary the runs as desired simply by modifying the configuration file, and thus to easily experiment with many different scenarios.

There are several standard formats for data modelling (XML, JSON, YAML, etc.), which, incidentally, are not used solely in the context of programming.

For this project, we have chosen to use the JSON format to list the simulation’s characteristic parameters.

The configuration file associated with this stage of the project is located at res/app.json.

We provide all the necessary functionality for retrieving data from such a file in the src/JSON directory.

Parameters are labelled hierarchically using strings that describe them.

For example, parameters relating to nutrients are expressed in this form

"nutrients" : {
      "growth" : {
            "speed" : 5,
             "max temperature" : 60,
             "min temperature" : 30
            },
       "quantity" : {
             "max" : 50,
             "min" : 40
            },
       "texture" : "foodA.png"
}
which reads as:

The getAppConfig() function in Application.hpp allows you to access the value of a given parameter. You must specify the value you are looking for using all the character strings that describe it, using the indexing operator .

For example, to access the maximum temperature at which a nutrient source can grow, you would write :

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

The resulting value must then be converted to the expected type using the provided conversion methods (toDouble() for doubles, toString() for strings, toBool for booleans, etc.) For example:

getAppConfig()["nutrients"]["texture"].toString()
(because the texture is supposed to be a string) or
getAppConfig()["nutrients"]["growth"]["max temperature"].toDouble()
(because the temperature is supposed to be a double)
Finally, note that if you do not specify all the descriptive strings, you will receive all the unspecified values in the form of an array. For example :
getAppConfig()["nutrients"]["growth"]
returns an array containing the values associated with "speed", "max temperature" and "min temperature" ({5, 60, 30} in the provided app.json file)

Note also that to simplify the syntax for accessing parameters, it is possible to define constants relating to them in the Config.hpp file.
For example, a shortcut has been created for the parameter getAppConfig()["simulation"]["time"]["factor"], which can be written as getShortConfig().simulation_time_factor" (see how this is coded in Config. [hpp][cpp]).

Please note that these shortcuts have only been created for a few parameters; you are free to add more later in Config.[hpp][cpp]).

Why not just use constants ?

The disadvantage of using simple global constants is that every time you want to change their value, you have to recompile and restart the programme. To get round this limitation, the simulation core provided (Application) features a Config attribute and the methods getAppConfig() and getShortConfig(), which allow you to retrieve values from the JSON configuration files. To change a value during a simulation, without having to recompile the code, simply edit the JSON file and reload it using the 'C' key ('C' for "Configuration").
Each graphical test you run can be configured using the desired configuration file, for example, via the command line:
       ./nutrientTest appTest.json
     
it will allow you to run the application using a file named appTest.json as the configuration file. If you do not specify anything, the file used will be the one whose name corresponds to the constant DEFAULT_CFG defined in Config.hpp. In the provided materials, this is app.json, but you are free to change it.

We can now return to the main focus of our work.

The class Nutrient

You are now asked to complete the Nutrient class, which will be used to model the sources of nutrients consumed by bacteria. To simplify matters, we will assume for the time being that they all eat the same thing !

[Question Q2.6] Like almost all the entities to be modelled, our nutrient sources will also be circular boundaries. How do you propose to use the provided class CircularBoundary (in the directory Lab/) to model this aspect ? Answer this question in your REPONSES file.

The Nutrient class will be specifically characterised by a quantity (of available nutrients). You will assume that a nutrient source knows which box it belongs to (for simplicity, its index within the set of boxes in the laboratory).

The provided file Utility/Types.hpp contains the definition of a number of predefined types, including the Quantity type, which you can make use of here.

[Question Q2.7] What is the point of using the Quantity type rather than simply a double? Answer this question in your REPONSES file.

You will implement the Nutrient class with :

Feel free to add other methods later if you think it’s necessary.

Drawing a Nutrient

A number of textures are provided in the res directory.

The Application::getAppTexture method allows you to load a texture from this directory into an object of type sf::Texture (for more on texturing, see the tutorial example).

Furthermore, the provided file Utility.[hpp][cpp] offers a method buildSprite that allows you to construct a textured object to be displayed.

The code for displaying a nutrient source should therefore look like this :

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

Display in "debugging" mode

You will be asked to provide certain additional displays in the project to facilitate certain tests. Your programme should therefore be able to run in "debugging" mode or not.

The configuration file contains a parameter "debug" that you can use for this purpose (and the provided file Application offers the function isDebugOn()).

You are therefore asked to complete the display of the nutrient source so that if debugging mode is enabled (the parameter associated with the "debug" tag set to true), the quantity of nutrients associated with the source is also displayed.

To display text, you can use the sf::Text buildText function provided in Utility.[hpp][cpp].

Adding nutrients to the culture box

To see your display methods in action, all you need to do is enable the addition of nutrients to the culture box.

To do this, complete the CultureDish::addNutrient method so that it adds a Nutrient* to the culture dish’s collection of nutrient sources. The nutrient will only be added if its position allows it to be inside the dish (its circular outline is contained within that of the dish). In this case, the addNutrient method will return true to indicate that the nutrient has successfully found its place in the dish. Otherwise, no nutrient will be added and addNutrient will return false. To carry out the necessary tests, you will use the methods programmed in CircularBoundary ensuring your code is properly modularised!

[Question Q2.8] When examining the test code provided for this section (src/Tests/GraphicalTests/NutrientTest.cc) which method needs to be added to the Laboratory class to allow nutrients to be added to the culture dish when the 'N' key is pressed, once the relevant line has been uncommented ? Which existing method needs to be modified to allow the newly added nutrient sources to be drawn? Answer these questions in your REPONSES file, then implement this method.

Test 3  : Nutrients (display and consumption)

To test your displays, uncomment the lines of code in the NutrientTest test file that correspond to the programming of the 'N' and 'T' keys.

Then run the test as before, ensuring you have set the application to «debugging» mode. This mode can be enabled or disabled using the 'D' key.

By moving your mouse to a position in the environment and clicking on 'N', you should see a food source appear there.

The 'T' key allows you to collect a quantity of 15 from the last nutrient source created. By pressing 'T' repeatedly, you should see the amount of nutrients decrease until it reaches 0. Pressing the 'T' key should then have no effect.

Please ensure that attempts to add nutrients too close to the edge of the box or outside it have no effect.

Also check that the quantities are not displayed as text when «debugging» mode is disabled.

An example of the test procedure is provided in the following short video:
← Pressing 'T' reduces the value of the most recently added nutrient source by 15 until it reaches 0.

Please note that to display the map in a larger size, simply change the "world" size ("size") in the app.json file
  "simulation" : {
        ....
        "background" : "sand.png",
	"debug background" : "sand.png",
        "size" : 800
    }

Nutrient growth as a function of temperature

So far, we have used the drawOn methods to display the elements of our simulation graphically; however, these remain, for the time being, static and unchanging.

We will now look at the update method to start adding a bit of dynamism to our simulation.

Start by adding the method void update(sf::Time dt) to your Nutrient class, which calculates the change in the nutrient after a time step of dt has elapsed.

The change will consist of increasing it by an amount:

auto growth = speed * dt.asSeconds();

where speed is the double value of the parameters associated with the tags ["nutrients"]["growth"]["speed"] in the configuration file.

The growth constraints are as follows :

  1. Growth will only be possible if the temperature of the culture box is between the limits minTemp and maxTemp, where minTemp and maxTemp are the values associated with the parameters labelled respectively ["nutrients"]["growth"]["min temperature"] and ["nutrients"]["growth"]["max temperature"].
  2. The quantity cannot increase beyond twice the value associated with the parameter labelled ["nutrients"]["quantity"]["max"]
  3. The nutrient source cannot extend beyond the box (which is the case if its bounding circle is not entirely contained within that of the box).

[Question Q2.9] Given that the Laboratory class does not provide access to its CultureDish objects via getters. How can you enable Nutrient::update to know the temperature of its own culture dish (or to know the limits so as not to exceed them) using Application::getAppEnv  ? Answer this question in the REPONSES file and write the necessary code.

Temperature control

Just as with selecting the culture dish, the temperature of the culture dish is a parameter that can be controlled directly via the graphical interface. Ensure that the value added to or subtracted from the temperature is the one associated with the label ["culture dish"]["temperature"]["delta"] and ensure that the temperature lies between the two limits given by the labels ["culture dish"]["temperature"]["min"] and ["culture dish"]["temperature"]["max"]. Next, check that your methods Lab::increaseTemperature and Lab::decreaseTemperature are correctly coded when you select temperature as the parameter to control. Also check that the temperatures of the different boxes can each be controlled independently (each box can have its own temperature).

Finally, complete the Lab::resetControls method so that it resets the temperature of the current box to the default temperature (the one specified when the box was created). The reset method should also achieve this effect (without duplicating code!).

Test 4  : Nutrients (growth and multiple boxes)

To test your latest developments, run the test as before and create nutrient sources by pressing the 'N' key. They should still be frozen ! The reason is that nutrients only grow once the temperature reaches 30 (see the app.json file):

 "nutrients" : {
	"growth" : {
            "speed" : 5,
            "max temperature" : 60,
            "min temperature" : 30
 },
  
Turn up the temperature using the settings you have programmed; you should see the food sources start to grow from the value 30.

← The temperature is increased in the first box, then we move on to the second, where we raise the temperature and then lower it to halt growth. In the third box, the temperature is kept constant. As we move from one box to the next, we see different contents and temperatures.
[Video : Nutrient growth as a function of temperature
& cultures in several boxes]
Use the menu buttons to check that growth is effectively halted when the temperature conditions are not met. Also check visually that the imposed growth limits are being enforced : a nutrient source cannot spill out of the box and does not grow beyond twice the maximum quantity associated with ["nutrients"]["quantity"]["max"] (set to 50 in the provided settings file).

Check that the 'R' key does indeed empty the current box and reset its temperature to the default value.

Finally, check that each bin can have its own composition and temperature, and that switching between them preserves these settings.

To fully understand the effects of the 'C' key, whilst your programme is still open, change the value of ["simulation"]["time"]["factor"] in the configuration file (for example, set it to 5). Press 'C' to reload this new value into the simulation. If you create nutrients with the correct temperature conditions, you should see them grow much faster. All simulation parameters can thus be modified whilst the programme is running, without having to restart it.

A minor modification to the code for CircularBoundary

In programming, it is often necessary to re-evaluate previous decisions and make useful changes to existing code ("code refactoring"). This is all the more necessary in the context of this project, as you may not necessarily be familiar with all the relevant concepts when tackling a particular section.

If you did things correctly last week, your CircularBoundary class was programmed by declaring the attributes as private and the methods as public.

To improve this design, make the changes to ensure that:

are only possible in its subclasses (and don’t forget to check that your programme still works once these changes have been made ;-))
Back to the project brief (part 2)


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