We’ve all heard of Darwin’s theory of evolution. Species slowly change in their physical characteristics in response to their environment, and the modifications (mutations) that offer an edge are the ones that survive. This phenomenon goes by the name of natural selection, or survival of the fittest. Wouldn’t it be great to see this happen before our eyes? Sadly, natural evolution often takes centuries and humans don’t stick around long enough to see it happen. What we can do, however, is simulate this. How? Through agent-based modelling of course!
So, what is Agent-based modelling?
Agent-based models (ABMs) are simulations that consist of many agents, all acting based on a predefined set of rules, and are simulated in discrete steps. Each agent acts independently based on its environment including the other agents around it. This allows us to analyse the effect of individual agent behaviour on the model as a whole.
Why not just formulate this mathematically, then? Well, it’s often easier to lay down rules for how agents should behave, rather than formulate sensible relationships between an arbitrary set of variables that define our model. This becomes even more relevant when there are a relatively large number of such variables.
Alright. How to go about making an ABM then?
For scientific and numerical applications, I prefer to use Julia. In the words of Wikipedia:
Julia is a high-level, high-performance, dynamic programming language.
What this means, is that Julia is easy to write, like Python, and runs fast, unlike Python. It’s also particularly geared towards number crunching and scientific applications. Indeed, Julia has an exceedingly advanced scientific machine learning and numerical modelling framework, courtesy of the code-magicians at SciML.
For my purposes, however, I turned to Agents.jl, a pure-Julia library for agent-based modelling. It has an extensive set of features, an easy-to-use API and is exceedingly fast.
So what does my model do?
My model consist of bacteria acting as agents, on a two-dimensional grid. The bacteria eat food in the grid cells to survive, and reproduce through binary fission, with a random variation in their parameters.
Each bacterium has the following properties:
age: How old the bacterium is.
energy: The current amount of energy a bacterium has.
sensory_radius: How far away the bacterium can spot food.
reproduction_threshold: How much energy a bacterium needs to reproduce through binary fission.
speed: How fast the bacterium can move, measured as the maximum number of steps it can take in any direction. The amount a bacterium eats in one iteration is also proportional to this parameter.
food_target: The location of the food source that a bacterium is moving toward.
The food is a regenerating resource at each grid tile. A tile with no food is an “empty tile”, and does not regenerate food. Any cell with non-zero food is a “food tile” and regenerates its food every iteration, up to a parameterised cap. The growth of food can be thought of like a fungus. The food tiles can spread to neighbouring empty tiles, causing them to start regenerating food. The probability for this spread is proportional to the amount of food the food tile has. This is like how a fungus grows around itself. Any empty tile can also become a food tile with a small, random probability. Think of this like a fungus spreading spores far away.
The rules governing the behaviour of the bacteria are simple:
- If a bacterium grows too old (as determined by a global cap on lifetime), or runs out of energy, it dies.
- If it has energy above its
reproduction_threshold, it reproduces through binary fission. In this process, each child inherits the parent’s
speedwith some genetic variation. Additionally, the energy of the parent is distributed between the two children, after removing a fixed cost of reproduction.
- If the bacterium is currently on a cell with some food, it will eat some food and gain energy. The amount of food it eats is proportional to its
- The bacterium will look for food. If it sees food, it will move toward it. Otherwise, it will move around randomly.
- Each iteration, if not eating, the bacterium loses some energy proportional to its sensory radius and distance moved that iteration
The expectation is that the bacteria will show random variation in their parameters over time, and the more successful combinations will survive. This would demonstrate the process of natural selection.
What happens when the model runs?
For the food concentrations, I used a 100 x 100 image, where each pixel corresponds to a grid tile, and the RGB value of a pixel determines the amount of food in the tile. This heatmap shows how the food is distributed:
There are four relatively small but concentrated spots of food, and one large area with less food per tile.
I configured the model to start with 10 identical bacteria distributed randomly on the map, and ran the simulation. The exact configuration corresponds to the
config.bson config file, run using the
runconfig function. Data from the simulation is constantly logged, and the following two visualisations capture a lot of what’s going on:
First, I made a video of how the food distribution changes over time.
This video shows 250 steps of a simulation that I run for 10,000 steps. The pattern continues for the rest of the duration. What’s interesting is how the bacteria finish the initial food distribution rapidly, and for the rest of the simulation it appears almost like ripples repeatedly sweeping across the grid, eating everything. The bacteria eat all the food faster than it regenerates. This is supported by the following plot:
The population spikes at first. This is due to the large amount of food available initially. Once the food is finished, the population drops steeply. After that, the population seems to oscillate, from nearly extinct to around 1000 bacteria. This corresponds to the “ripples” observed in the food distribution animation. Every time a ripple occurs, the bacteria eat a lot of food, thus reproducing more. Once the food finishes, a lot of the bacteria die due to starvation. As the number of bacteria decrease, the food demand decreases and the amount of food increases. Hence, the cycle continues.
It’s interesting to note that the average speed value is somewhat in-sync with the population. During a population boom, bacteria with higher speed survive. This can be explained by the fact that population rises when food is abundant, and bacteria are rewarded for being able to get to food faster and eat more. Once food becomes scarce, the bacteria that consume less energy roaming around looking for food are more likely to survive.
Reproduction threshold increases fairly steadily over time. This implies that over time it becomes more difficult to reproduce, but the two child bacteria have more energy to start off with. This is interesting, since it means that despite periodic and frequent “famines”, it’s still more advantageous to wait longer and eat more. This could be due to the fact that children that start with less energy also don’t survive long enough to get food. Another possibility is that the bacteria reach their threshold long before they become too old, so it’s beneficial to exploit the longer lifetime further.
Sensory radius has a less steady, yet still marked growth during the course of the simulation. A larger sensory radius allows bacteria to see food further away, at the expense of a greater energy cost each iteration. Consequently, bacteria that can see further can take more advantage of their higher speed. What’s more interesting to me, however, is that the sensory radius appears to settle at around 6 for most of the simulation, and only increases near the end. I’m yet to explain this, and would love inputs.
This model was fun to make, and even more fun to explore. I expected the population and parameters to stabilise to “ideal” values, and the oscillatory behaviour was a surprise. However, what fun is a simulation if it does what you expect it to? Analysing the graphs for this proved much more of a challenge than if they simply converged.
A great deal of thought went into figuring out how energy should be gained and expended, and I still think there’s a more appropriate alternative I haven’t yet arrived at. Additionally, I tried adding multiple species of bacteria, each of which start off with a different combination of parameters. Every time, all but one species died off within a few hundred iterations, and the other species behaved similarly to the above analysis. It would’ve been nice to see coexistence, if it’s even possible through this model.
All of the code for this simulation, including data collection and plotting, is available in this GitHub repository. If anyone makes any interesting modifications, or finds a configuration that leads to interesting results, I’d love to see the results. If you have any suggestions, drop a comment below!