Alien Planet¶

Ever since reading Yuval Noah Harari's 'Sapiens', I had been toying with the idea of creating a simulated world which would explore migrations and population growth. The inhabitants of our world will be randomly allocated within their environment and allowed to move in any direction by one unit, with the exception of inaccessible terrain (which has also been randomly generated). For some reason, I had decided that the inhabitants are actually aliens experiencing the cradle of their existence, trying to make sense of their surroundings. Although the aliens have not been endowed with much complexity, I have granted them the ability to effectively do three things:

  • They can migrate.
  • They can reproduce.
  • They can eliminate each other.

That being said, there are certain rules to their activities. Reproduction can only take place between males and females and seemingly, only the males attack each other (as I am writing this, I wonder how ridiculous this all sounds and yet...) The initial conditions of our simulated world may be modulated but for this particular set-up, I have chosen a 100-year span to explore the variations in experience our alien population may come to face.

In [45]:
import numpy as np
import copy
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator

for sim in range(0, 3):
    # Create n x n grid with randomised inhabitable zones
    n = 15
    grid = np.random.uniform(low=0, high=5, size=(n,n))
    grid = np.where(grid < 1, np.nan, 0)

    # Create p initial lifeforms which are randomly allocated on the grid
    p = 5
    initial_coordinates = list()

    for _ in range(0, p):
        x = np.random.randint(0, n)
        y = np.random.randint(0, n) 
        initial_coordinates.append([x,y])

    for coordinate in initial_coordinates:
        grid[coordinate[0]][coordinate[1]] = 1

    # Class for floopy aliens
    class floopy():
        def __init__(self, initial_coordinates, grid):
            self.coordinates = initial_coordinates
            self.grid = grid
            self.gender =  np.random.choice(['Male', 'Female'])

        def migrate(self):
            dx = np.random.randint(-1,2)
            dy = np.random.randint(-1,2)
            if self.coordinates[0] + dx < len(grid) - 1 and self.coordinates[0] + dx >= 0:
                if self.coordinates[1] + dy < len(grid) - 1 and self.coordinates[1] + dy >= 0:
                    if not np.isnan(grid[self.coordinates[0] + dx][self.coordinates[1] + dy]):
                        self.coordinates[0] = self.coordinates[0] + dx
                        self.coordinates[1] = self.coordinates[1] + dy       
            return self.coordinates

    # Initialise population
    population = []

    for i in range(0, p):
        population.append(floopy(initial_coordinates[i], grid))

    def show_grid(array, iteration, ax):
        ax.imshow(array, cmap='magma', interpolation='nearest', aspect='auto', vmin=0, vmax=5)
        cbar = ax.figure.colorbar(ax.imshow(array, cmap='magma', interpolation='nearest', aspect='auto', vmin=0, vmax=5))
        cbar.ax.yaxis.set_major_locator(MaxNLocator(integer=True))
        ax.set_title(f'Year {iteration+1}')

    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
    show_grid(grid, 0, ax1)

    #  Run simulation
    for iteration in range(0, 100):
        if len(population) > 1000:
            print("Abort: population overload!")
            break
        for i in range (0, p):
            try:
                old_coordinates = copy.deepcopy(population[i].coordinates)
                new_coordinates = population[i].migrate()
                grid[old_coordinates[0]][old_coordinates[1]] = grid[old_coordinates[0]][old_coordinates[1]] - 1
                grid[new_coordinates[0]][new_coordinates[1]] = grid[new_coordinates[0]][new_coordinates[1]] + 1
                if grid[new_coordinates[0]][new_coordinates[1]] > 1:
                    intersecting_floopies = [(index, obj) for index, obj in enumerate(population) if obj.coordinates == new_coordinates]
                    if intersecting_floopies[0][1].gender != intersecting_floopies[1][1].gender:
                        grid[new_coordinates[0]][new_coordinates[1]] = grid[new_coordinates[0]][new_coordinates[1]] + 1
                        population.append(floopy(new_coordinates, grid))
                        p = p + 1
                        #print("A floopy has been born!")
                    else:
                        if intersecting_floopies[0][1].gender == 'Male':
                            grid[new_coordinates[0]][new_coordinates[1]] = grid[new_coordinates[0]][new_coordinates[1]] - 1
                            population.pop(np.random.choice([intersecting_floopies[0][0], intersecting_floopies[1][0]]))
                            p = p - 1
                            #print("A floopy has died!")
            except:
                continue
                
    # Show final grid
    male_population = len([obj for obj in population if obj.gender == 'Male'])
    female_population = len([obj for obj in population if obj.gender == 'Female'])
    show_grid(grid, iteration, ax2)
    print("Simulation", sim+1, "- The final population is", male_population, "male(s) and", female_population, "female(s).")
    plt.show()
Simulation 1 - The final population is 3 male(s) and 4 female(s).
Simulation 2 - The final population is 36 male(s) and 44 female(s).
Abort: population overload!
Simulation 3 - The final population is 576 male(s) and 646 female(s).

Discussion¶

The above grids represent a 2-dimensional planet. White squares represent inhabitable zones, black squares are available for free movement and all the colours in between represent population.

Despite similar starting conditions, the final outcome of our three simulated planets look drastically different. In the first simulation, advanced civilisation has failed to arise with only two additional inhabitants occupying the planet after a century of migration. I could dig a bit deeper into what happened here but what I would presume is that the initial inhabitants migrated with meager interactions in between leaving few opportunities to reproduce. The second simulation sees greater progress with clusters of population emerging towards the corners of our map (I like to think of these clusters as alien hubs or cities). These 'cities' appear when Goldilocks conditions allow for positive feedback loops to occur which rapidly accelerate the average birth rate. Since the aliens effectively follow a random walk, their expected position is most likely to be where they currently are. This means that newborn aliens are most likely to remain in close proximity to the birth hubs, thus increasing the likelihood of further reproduction in the area (no, I have not corrected for incest). The final simulation shows what happens when this exponential growth effect goes unchecked rapidly filling the grid with new members. I had to create a particular catch statement to abort the simulation when this happens because the loop of new births ends up stalling my system!

My main takeaway from this analysis (apart from the fact that I could grow attached to byte-sized simulated aliens) is the importance of non-linear dynamics in determining the final shape of the simulation. The longer the simulation is allowed to ride, the more likely it is for these 'chaotic' features to appear; however as we have seen from the first simulation, time is non-uniform and the realisation of such may be a mix of random chance, as well as the initial attributes of each starter world. For example, initial population proximity or specific geographical features which might restrict migration would increase the likelihood for these conditions to manifest, although there is no guarantee that this should be the case.

If the inhabitants of Simulation 3 were to look back and reflect on the unprecedented growth they'd seen in the past century, they might conclude that this was meant to happen by some turn of fate or deterministic willing, but our more holistic view of only two other simulations seems to tell us otherwise. It's just one path on a very unruly trail.

In [ ]: