Goals: Allow nutrient sources to grow spontaneously in the culture box, depending on the temperature.
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)
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]).
./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.
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).
[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 :
getConfig()["quantity"]["max"]rather than
getAppConfig()["nutrients"]["quantity"]["max"]
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);
auto const& texture = getAppTexture(getConfig()["texture"].toString());
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.
auto const text = buildText(...); target.draw(text);
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.
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. |
"simulation" : {
....
"background" : "sand.png",
"debug background" : "sand.png",
"size" : 800
}
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 :
[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!).
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] |
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.
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: