Automata in nature

Simple automata can be found in the least expected places. At the beginning of the 1950's, Alan Turing started working on mathematical models that could explain how antagonistic chemical reactions could help explain the patterns and shapes observed in the skin pigmentation of some animals and plants. This was later called the reaction-diffusion algorithm.
But some examples of cellular automata are less discrete. We will try to implement the waving behavior observed in wild colonies of bees. This behavior is relatively common in social arthropods, and it's intended as a deterrent for predators that may get scared when looking at a colony as a single, bigger, and more aggressive organism.
The basic unit of information in the bee-wave system is a bee. A bee can have its wings open or closed, and can be receptive to the stimulus of its neighbors, or not. A bee that is receptive, and doesn't have its wings open reacts to any of the neighbors in its vicinity that may have raised their wings by doing it too, propagating the behavior throughout the colony. After reacting, the bee takes a couple of seconds to rest, preventing it to backpropagate the signal and enter an infinite loop.



The parameters of the Algorithm

  • RES: int - tile size
  • DIMS: (int,int) - Number of columns and rows of tiles that make the grid
  • SCREEN: (int,int) - Dimensions of the screen in pixels
  • FPS: int - Frames per second of animation
  • TIME_BEE_REST: float - amount of time the bee will remain in state 0 before being able to change state
  • TIME_BEE_ACTIVE: float - amount of time the bee will remain in state 1
  • TILE: Tile() - Class Tile that represent the basic unit of information in the system
  • GRID: [[TILE, ..., n=DIMS[0]], ..., n = DIMS[1]] - a 2D matrix of TILE with dimensions given by the columns and rows of DIMS
# MODULE
import pygame
import random

# DD
DIMS = (32,32)
RES = 16
SCREEN = (DIMS[0]*RES, DIMS[1]*RES)
display = pygame.display.set_mode(SCREEN)
FPS = 10
TIME_BEE_REST = 1
TIME_BEE_ACTIVE = 0.1

# DD. TILE
# tile = Tile()
# interp. a tile in the grid
class Tile:
    def __init__(self,c,r):
        self.c = c 
        self.r = r 
        self.x = self.c * RES
        self.y = self.r * RES
        self.state = 0
        self.nextState = 0
        # attr. related to waiting periods of activity
        self.timerInState1 = 0 #Time the bee stays in a state 1
        self.resetTimerInState1 = TIME_BEE_ACTIVE
        self.recoilTimer = 0    #Time the bee rests before activating it again
        self.resetrecoilTimer = TIME_BEE_REST
        # attr. related to pixel rendering and collision detection
        self.rect = pygame.Rect(self.x, self.y, RES, RES)
    
    def draw(self):
        pygame.draw.rect(display,self.getColor(),self.rect)


    def getColor(self):
        if self.state == 0:
            return ("white")
        return "black"
    
# DD. GRID
# grid = [[TILE, ..., n=DIMS[0]], ..., n=DIMS[1]]
# interp. a 2D array of tiles arranged in a grid
grid = []
for r in range(DIMS[1]):
    row = []
    for c in range(DIMS[0]):
        tile = Tile(c,r)
        row.append(tile)
    grid.append(row)

# TEMPLATE FOR GRID
# for row in grid:
#   for tile in row:
#       ... tile

The Algorithm

  • For every tile in the system:
    • If the tile has state 0
      • T: Does the tile have any neighbors with state 1? Is the tile ready to change to state 1?
        • make the NEXT state 1
      • F: reduce the timer resting timer and make it closer to 0
    • else: tile has state 1
      • Is it time for the state 1 to end?
        • T: make the NEXT state 0 and reset all timers to TIME_BEE_ACTIVE and TIME_BEE_REST, respectively
        • F: reduce the time the tile has left being active, with state 1
  • For every tile in the system:
    • Make NEXT state the CURRENT state

Let's implement the algorithm using python!
# MODULE
import pygame
import random

# DD
DIMS = (32,32)
RES = 16
SCREEN = (DIMS[0]*RES, DIMS[1]*RES)
display = pygame.display.set_mode(SCREEN)
FPS = 10
TIME_BEE_REST = 1
TIME_BEE_ACTIVE = 0.5

# DD. TILE
# tile = Tile()
# interp. a tile in the grid
class Tile:
    def __init__(self,c,r):
        self.c = c 
        self.r = r 
        self.x = self.c * RES
        self.y = self.r * RES
        self.state = 0
        self.nextState = 0
        # attr. related to waiting periods of activity
        self.timerInState1 = 0 #Time the bee stays in a state 1
        self.resetTimerInState1 = TIME_BEE_ACTIVE
        self.recoilTimer = 0    #Time the bee rests before activating it again
        self.resetrecoilTimer = TIME_BEE_REST
        # attr. related to pixel rendering and collision detection
        self.rect = pygame.Rect(self.x, self.y, RES, RES)
    
    def draw(self):
        pygame.draw.rect(display,self.getColor(),self.rect)


    def getColor(self):
        if self.state == 0:
            return ("white")
        return "black"
    
# DD. GRID
# grid = [[TILE, ..., n=DIMS[0]], ..., n=DIMS[1]]
# interp. a 2D array of tiles arranged in a grid
grid = []
for r in range(DIMS[1]):
    row = []
    for c in range(DIMS[0]):
        tile = Tile(c,r)
        row.append(tile)
    grid.append(row)

# TEMPLATE FOR GRID
# for row in grid:
#   for tile in row:
#       ... tile





# CODE

def draw():
    display.fill("green")
    for row in grid:
        for tile in row:
            tile.draw()
    pygame.display.flip()
    pygame.time.Clock().tick(FPS)

def update():
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()

        if event.type == pygame.MOUSEBUTTONUP:
            mx,my = pygame.mouse.get_pos()
            for row in grid:
                for tile in row:
                    if tile.rect.collidepoint(mx,my):
                        tile.state = 1
                        tile.getColor()
            



    for row in grid:
        for tile in row:
            r = tile.r
            c = tile.c
            ns = [
                grid[r-1][c-1],grid[r-1][c],grid[r-1][(c+1)%DIMS[0]],
                grid[r][c-1],               grid[r][(c+1)%DIMS[0]],
                grid[(r+1)%DIMS[1]][c-1],grid[(r+1)%DIMS[1]][c],grid[(r+1)%DIMS[1]][(c+1)%DIMS[0]]
                  
                ]
            
            
            # if the bee is ready
                # if at least one neighbor is active, and bee is not resting, activate it
                # reduce the time left for the bee to rest
            # else: 
                # if the timer for staying in state 1 ran out
                    # reset timer to stay in state 1, change state to 0 and put the bee to rest
                # else: update timer to make state 1 closer to 0
            
            if tile.state == 0:
                # if there is at least one neighbor active, and tile is not already active, and bee is not resting, activate
                if sum([n.state for n in ns]) >0 and tile.recoilTimer<0:
                    tile.nextState = 1
                else:
                    tile.recoilTimer -= 0.1
            else:
                if tile.timerInState1 < 0:
                    tile.timerInState1 = tile.resetTimerInState1
                    tile.nextState = 0
                    # put the bee to rest
                    tile.recoilTimer = tile.resetrecoilTimer
                else:
                    tile.timerInState1 -= 0.1

    for row in grid:
        for tile in row:
            tile.state = tile.nextState
            


while True:
    draw()
    update()