# Simulate evolution of a group of cells

The following code simulates the (simplified) concept of evolution of cells. They can reproduce, grow older and have a fitness which modifies their chance of survival. Using , graphs are drawn which show the amount of cells as well as their average fitness. Several values can be modified to change the initial cells.

What could be improved about it?

import random

import matplotlib.pyplot as plt

START_AMOUNT = 250
START_FITNESS = 0.1
MAX_CELLS = 10000

class Cell:
"""
Cell max. age: 3
0 - nothing
1 - reproduce
2 - reproduce
3 - die
Fitness is a float between [0, 1].
"""

def __init__(self, fitness: float):
self.age = 0
self.fitness = fitness

cells = [Cell(START_FITNESS) for _ in range(START_AMOUNT)]
stat_cells = []
stat_avg_fitness = []
generations = 0

while 1:
# visuals
if not len(cells):
print("All cells have died!")
break
avg_fitness = sum(c.fitness for c in cells) / len(cells)
print(f"{len(cells)} living, avg. fitness: {avg_fitness}")
stat_cells.append(len(cells))
stat_avg_fitness.append(avg_fitness)
generations += 1
plt.figure(1)
plt.plot(stat_cells)
plt.ylabel("cells")
plt.title("Cell amount")
plt.figure(2)
plt.plot(stat_avg_fitness)
plt.ylabel("avg. fitness")
plt.title("Avg. fitness")
plt.pause(0.05)
if len(cells) > MAX_CELLS:
print(f"More than {MAX_CELLS} cells reached, aborting!")
break

new_cells = []

for cell in cells:
# get older
cell.age += 1
# die if too old or randomly
if cell.age == 3 or random.random() > cell.fitness:
continue
# reproduce
if cell.age in (1, 2):
new_cells.append(Cell(cell.fitness))
# mutate
cell.fitness += random.uniform(-1, 1) * random.uniform(-1, 1) * random.uniform(-1, 1)
if cell.fitness < 0:
cell.fitness = 0
if cell.fitness > 1:
cell.fitness = 1
cells = [c for c in cells if c not in dead_cells]
cells.extend(new_cells)

plt.show()


# use functions

The three manifest constants, up through MAX_CELLS, are at module level. Fine. But the while loop really should be within a function. That would let others, such as unit tests, safely import this module.

# use math

We are examining individual cell dynamics, that's fine. But they don't interact with neighbors, it's not like we have Life here. I bet you could write analytic formulas describing aggregate behavior of all cells in a given time step, or across N steps.

    avg_fitness = sum(c.fitness for c in cells) / len(cells)


This is perfectly nice, there's no need to change it.

When writing idiomatic python some folks prefer to phrase it using attrgetter('fitness').

# use appropriate datastructure

This is horribly expensive; it accidentally falls into the wrong complexity class:

    dead_cells = []
...
...
cells = [c for c in cells if c not in dead_cells]


Please understand that the in operator represents a nested loop here. So if mortality during one generation of N cells was M dead cells, say a thousand, instead of O(N) we perform O(N × M) work to evaluate that boolean expression. Don't do that.

Define dead_cells to be a set(), augment them with .add(), and then in shall complete in O(1) constant time, putting us back at O(N) work.

# cite a reference

... random.uniform(-1, 1) * random.uniform(-1, 1) * random.uniform(-1, 1)


Interesting distribution you've got, there.

It would be helpful to cite a reference so we understand its shape, and why it's relevant to the problem at hand.