3
\$\begingroup\$

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 = []
    dead_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:
            dead_cells.append(cell)
            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()
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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 = []
            ...
            dead_cells.append(cell)
            ...
    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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.