Introduction to evolution

Evolution: Life’s most clever algorithm

Evolution is the process by which populations change over generations through variation, inheritance, and differential survival. This idea, famously championed by Darwin and Wallace, explains the diversity of life on Earth. It describes how species adapt to their environments, how new species arise, and how complex traits evolve. Today, the concept of evolution has expanded beyond biology, it’s recognised as a powerful algorithm that drives adaptation in systems ranging from bacteria (genes) to ideas (memes), from DNA (nucleotides) to computer code (bits).

In this first part of the course, we’ll bring these ingredients to life by writing our own simulations and watching evolution unfold on the screen. And while our digital creatures aren’t made of flesh and blood, the evolutionary battles they fight, the strategies they discover, and the adaptations they evolve are as real, and often as surprising, as anything found in nature itself.

Three ingredients

As briefly mentioned above, we just need three ingredients to have evolution by means of natural selection:

  • variation (differences between individuals),
  • inheritance (the passing on of traits),
  • selection (some variants performing better than others).

The last ingredient is self-evident. Evolution by means of natural selection requires selection. It is especially the first two that are a little more tricky to really understand, as they are not always as obvious as they seem.

Balancing change and stability

To evolve, a system needs enough variation – if everyone is the same, there’s nothing for selection to act on. But this variation can’t just be noise; it needs to be passed on. That means inheritance can’t be perfect – there must be room for change, such as through mutations – but it also can’t be too sloppy. If traits aren’t reliably transmitted to the next generation, then even the best adaptations will vanish before they can take hold. Evolution therefore acts within a sweet spot: replication must be good enough, but not perfect. Just enough “memory” and just enough “change”. To make this a little more tangible, let us make our very first simulation.

A simple evolutionary algorithm

One simple way to simulate evolution is with a Moran process, a classic model from population genetics. Imagine a population of 100 individuals, each with a single gene that determines its fitness. This gene can have all values from 0 to 1 (let’s call this value \(\phi\)). At each time step, one individual is chosen to reproduce with a probability proportional to \(\phi\), producing 1 offspring. This offspring inherits their parents gene (so the same \(\phi\)), but with a probability \(\mu\), the value changes by a small amount (a mutation). The population size will now be 101, which could be interesting if we want to study population growth. However, in a Moran process we keep it simple: one random individual is removed by the new offspring, so the population size is constant while still allowing fitter individuals to spread over time.

Below is a minimal Python snippet that does exactly what is described above. Read it carefully, as this is your very first ‘real’ Python script in this course.

Plotting with matplotlib

The code snippet above is a nice introduction: it sets the parameters for your model, performs a short loop, and reports statistics. But with many models in this course, looking at raw statistics is not going to be very informative. Humans are much better at interpreting graphs and other visuals, so for this we will use matplotlib to modify the script above and make a graph instead. Make sure to once again carefully read through this code, it is very similar to the code above but adds some plotting code. New elements are explained with # comments.

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(5)

N = 100 # Default population size 
mu = 0.001 # Default mutation rate
# Updated parameters
steps = 50000

def simulate(N=N,mut=mu): 
    avg_fitness = []
    fitnesses = np.full(N, 0.05)
    # Moran process with mutation (logging every 10 steps)
    for step in range(steps):
        probs = fitnesses / fitnesses.sum()
        parent = np.random.choice(N, p=probs)
        dead = np.random.choice(N)

        # Copy with mutation
        new_fit = fitnesses[parent]
        if np.random.rand() < mut:
            new_fit = np.clip(new_fit + np.random.normal(0, 0.1), 0, 1)
                
        fitnesses[dead] = new_fit

        # Save average fitness every 10 steps
        if step % 10 == 0:
            avg_fitness.append(fitnesses.mean())
            
    # Plotting
    plt.plot(np.arange(0, steps, 10), avg_fitness, label=f"mu={mut}")

simulate(N,0.0001)
simulate(N,0.001)
simulate(N,0.01)
simulate(N,0.1)
simulate(N,1)

# Plotting
plt.xlabel("Step")
plt.ylabel("Average fitness")
plt.title("Evolution of Fitness in a Moran Process")
plt.grid(True)
plt.tight_layout()
plt.legend()
plt.show()

Exercise 1 (Moran process simulation)

As you can see, the direct code ‘snippets’ are already getting quite long, and it would be better to get your script on your own laptop. Copy and paste the code to your own machine, and see if you can get it to run. Then answer the following questions:

  1. How “well adapted” is the initial population?
  2. The population fitness seems to plateau at 5000 time steps. Is that a real result? Investigate!
  3. Read how the mutations are implemented. Can you think of other ways to implement mutations?
  4. Can the parent be replaced by its own offspring? Why/why not?
  5. Try to decreasing/increase value of \(\mu\) (mutation rate). Which values makes evolution go faster? Which values make evolution more precise?

What this first part of the course is about

The above simulation is fun, but not really… biologically exciting. While some simplifications are necessary to make models feasible, we will investigate a few evolutionary models that are somewhat more exciting. We will discuss how to model spatial structure and local competition, how genotypes (where mutations happen) get translated into phenotypes (where selection happens), and how the environment can change over time and lead to niche construction and interactions.