latentspace.

The selfish gene

What is natural selection? 🦕

Natural selection is a mechanism of evolution. Organisms that are more adapted to their environment are more likely to survive and pass on the genes that aided their success. This process causes species to change and diverge over time. Natural selection is one of the ways to account for the millions of species that have lived on Earth.

For example, the evolution of long necks has enabled giraffes to feed on leaves that others cannot reach, giving them a competitive advantage. Owing to this better food source, individuals with longer necks were more likely to survive, while those with shorter necks and access to less food were less likely to survive.
Giraffes carrying different genes from one another
Giraffes carrying different genes from one another

Creating environment and defining genes

A simple simulation is built based on the basic concepts of natural selection. First, an environment and populations that carry the genes must be created. The worms live in a two-dimensional environment, and each worm has the following genes that allow it to survive by eating feeds:
The class is defined as below. Worms act through the moving function, which interacts with the genes.

    class WormGene:
        def __init__(self, speed: int, sense: float, switch: float) -> None:
            self.speed = speed
            self.sense = sense
            self.switch = switch


    class Worm(WormGene, WormHelper):
        def __init__(
            self, 
            speed: int = EnvironmentConsts.WORM_SPEED_ORIGINAL, 
            sense: float = EnvironmentConsts.WORM_SENSE_ORIGINAL, 
            switch: float = EnvironmentConsts.WORM_SWITCH_ORIGINAL
        ):
            WormGene.__init__(self, speed, sense, switch)
            self._initialize()
            

        (...)


        def moving(self, apples: List[Apple]) -> None:
            self.direction: str
            self.direction = self._get_random_direction(self.direction, self.switch)
            
            nearest_apple = self._get_nearest_apple(self.body[-1], apples, self.sense)
            if nearest_apple is not None:
                self.direction = self._get_direction_by_sense(self.body[-1], nearest_apple, self.direction)
            
            self.body: List[List[int]]
            self.body = self._get_moved_body(self.body, self.direction, self.speed)


The full code for the Worm class is available at the following link. The environment and worms operate as shown below in Fig.2: An operation of the environment and worms.
Depending on whether the worm's sensing circle contains apples, each worm follows a different operation method. If no apples are within the sensing circle, the worm determines its direction randomly based on the switch gene.

Otherwise, the angle between the worm's movement direction vector and the worm-to-apple vector is computed, and the angular difference is used to select the vector with the highest similarity. The code shows the details of the worm's operation method _get_direction_by_sense(), and the method for finding the direction most similar to an apple is described mathematically as follows:


An operation of the environment and worms
An operation of the environment and worms


Simulating natural selection

All preparations for the simulation are now complete. Natural selection can be simulated as shown below in Fig.3: A part of the natural selection simulation.

The generation, population, and seed are fixed for the reproducibility of the natural selection simulations, and the surviving genes are observed. Note that worms that do not eat even a single apple within one generation die.

A part of the natural selection simulation
A part of the natural selection simulation

The parameters for generation, population, and seed are set as shown below. The results are then logged and visualized to observe how genes evolve and survive.
The visualized results are examined below. The x-axis represents the generation and the y-axis represents each gene. In each figure, a line is drawn for the average genes per generation. The configured number of generations is 100, but the simulation stops at generation 70 because only one worm survives after that point.

The Speed and Sense genes tend to increase gradually over generations. Detecting apples and moving quickly to eat them is naturally advantageous for survival.

The Switch gene starts at 0.5, and the final evolved worm also returns to 0.5. In this environment, changing direction too frequently appears to work against survival.

Speed (1 of 3)
Speed

Speed (2 of 3)
Sense

Speed (3 of 3)
Switch


The birth of Super worm

Lastly, the data of the super worm that survived until the end of the environment is reproduced to conclude the post.
The super worm, which moves remarkably fast Speed: 13 Sense: 450 Switch: 0.48
The super worm, which moves remarkably fast
Speed: 13
Sense: 450
Switch: 0.48

References