[PYTHON] Completely understood Chapter 3 of "Making and Moving ALife"

Emergence of individuals

――How is the individual defined? —— “Opening and closing” is necessary for each individual -Receive changes in the environment by opening and maintain yourself by confining changes inside ―― “Opening and closing” is a structure that recursively refers to itself, and is necessary for the emergence of a living individual. ――A system that has a mechanism of "minimum unit of life" that can generate and maintain itself is called "autopoiesis".

SCL model

--One of the models that creates the concept of autopoiesis is the "SCL (Substrate Catalyst Link)" model. ――The question "What is emergence?" Shows one way of understanding that "it is the process that constitutes itself that determines the existence of oneself."

Model overview

――It consists of various molecules that move in a two-dimensional lattice and their chemical reaction formulas. --There are three types of molecules in the grid-like cells --Substrate: Green circle --Catalyst: Purple circle --Membrane molecule (Link): Blue square

スクリーンショット 2019-12-13 10.56.11.png (69.9 kB)

--Each molecule moves between cells and undergoes chemical reactions such as binding and decomposition with other adjacent molecules. - 1) 2S + C → L + C --A catalyst molecule (C) produces one membrane molecule (L) from two substrate molecules (S). - 2) L + L → L - L --The generated membrane molecule (L) is fixed in space by binding to the adjacent membrane molecule (L). - 3) L → 2S --The membrane molecule (L) is decomposed into the substrate molecule (S) again with a certain probability. --In the process of chemical reaction, the film is formed and maintained as a whole.

Let's take a look at how the "individuals" are formed and how they are maintained.

State of individual formation

--Catalyst molecules begin to form membrane molecules nearby --Membrane molecules bind to form a link, enclose the catalyst molecule and begin to form a membrane ――Begin to make a unit called "individual" just like a cell

Self-maintenance

--Substrate molecules move back and forth between the inside and outside of the membrane --When the substrate molecule in the membrane is converted into a membrane molecule by the catalytic molecule, a link is created inside the membrane. --By doing so, even if the link making the membrane is disassembled and a hole is created, the link in the membrane fills the hole and repairs the broken membrane.

Model implementation

The SCL model is implemented as a kind of two-dimensional cellular automaton. The cell can be in the following five states.

--CATALYST (catalyst molecule) --SUBSTRATE (Substrate molecule) --LINK (membrane molecule) --LINK-SUBSTRATE (Membrane molecule and substrate molecule coexist) --HOLE (empty)

The cell state is described in Python as follows. Items other than type will be described later.

{ 'type': 'LINK', 'disintegrating_flag': False, 'bonds': [(6, 5, 8, 5)] }

Molecular reactions and bonds can be expressed by the following six reactions. Details will be explained in the calculation part.

The parameters of the model are as follows.

MOBILITY_FACTOR = {
    'HOLE':           0.1,
    'SUBSTRATE':      0.1,
    'CATALYST':       0.0001,
    'LINK':           0.05,
    'LINK_SUBSTRATE': 0.05,}
PRODUCTION_PROBABILITY             = 0.95
DISINTEGRATION_PROBABILITY         = 0.0005
BONDING_CHAIN_INITIATE_PROBABILITY = 0.1
BONDING_CHAIN_EXTEND_PROBABILITY   = 0.6
BONDING_CHAIN_SPLICE_PROBABILITY   = 0.9
BOND_DECAY_PROBABILITY             = 0.0005
ABSORPTION_PROBABILITY             = 0.5
EMISSION_PROBABILITY               = 0.5

MOBILITY_FACTOR is the ease of movement of each molecule. _PROBABILITY is the probability of each reaction.

The implementation of the SCL model can be divided into three parts:

--1) Initialization ―― 2) Molecule movement ―― 3) Molecular reaction

Let's look at them in order.

Initialization

In the initialization phase, a two-dimensional array that stores cell information is prepared and molecules are arranged.

#Initialization
particles = np.empty((SPACE_SIZE, SPACE_SIZE), dtype=object)
# INITIAL_SUBSTRATE_Place SUBSTRATE and HOLE according to DENSITY.
for x in range(SPACE_SIZE):
    for y in range(SPACE_SIZE):
        if evaluate_probability(INITIAL_SUBSTRATE_DENSITY):
            p = {'type': 'SUBSTRATE', 'disintegrating_flag': False, 'bonds': []}
        else:
            p = {'type': 'HOLE', 'disintegrating_flag': False, 'bonds': []}
        particles[x,y] = p
# INITIAL_CATALYST_Place CATALYST in POSITIONS.
for x, y in INITIAL_CATALYST_POSITIONS:
    particles[x, y]['type'] = 'CATALYST'

First, we have prepared a two-dimensional array of SPACE_SIZE x SPACE_SIZE.

particles = np.empty((SPACE_SIZE, SPACE_SIZE), dtype=object)
particles 
# => array([[None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None],
       [None, None, None, None, None, None, None, None, None, None, None,
        None, None, None, None, None]], dtype=object)

Next, there is a certain probability that the type will be set to SUBSTRATE or HOLE for the case cell. ʻEvaluate_probability (possibility)` returns TRUE or FALSE with a probability of argument. This sets the type.

for x in range(SPACE_SIZE):
    for y in range(SPACE_SIZE):
        if evaluate_probability(INITIAL_SUBSTRATE_DENSITY):
            p = {'type': 'SUBSTRATE', 'disintegrating_flag': False, 'bonds': []}
        else:
            p = {'type': 'HOLE', 'disintegrating_flag': False, 'bonds': []}
        particles[x,y] = p

#Returns True or False according to probability probability
#Probability must be between 0 and 1
def evaluate_probability(probability):
    return np.random.rand() < probability

Finally, place the catalyst molecule.

for x, y in INITIAL_CATALYST_POSITIONS:
    particles[x, y]['type'] = 'CATALYST'

Molecule movement

Next, I will explain the movement of molecules. Molecule movement is achieved by swapping the information in two adjacent cells. Randomly selects one cell from the Neumann neighborhood of a cell and decides whether to move according to MOBILITY_FACTOR.

To supplement the Neumann neighborhood, the four cells on the top, bottom, left, and right are considered next to each other. On the other hand, eight cells, including diagonal ones, are next to Moore.

noiman.png (16.8 kB)

The method that actually represents the neighborhood is as follows.

def get_neumann_neighborhood_list(x, y):
    """
Gets a list containing the four coordinates near Neumann at the specified coordinates.

    Parameters
    ----------
    x : int
The x coordinate of the target.
    y : int
The y coordinate of the target.

    Returns
    -------
    neumann_neighborhood_list : list of tuple
A list containing the four coordinates near Neumann. Right, left, bottom, top in order
It is stored.
    """
    neumann_neighborhood_list = [
        ((x + 1) % SPACE_SIZE, y),
        ((x - 1) % SPACE_SIZE, y),
        (x, (y + 1) % SPACE_SIZE),
        (x, (y - 1) % SPACE_SIZE),
    ]
    return neumann_neighborhood_list

Now let's look at the implementation of movement.

moved = np.full(particles.shape, False, dtype=bool)
    for x in range(SPACE_SIZE):
        for y in range(SPACE_SIZE):
            p = particles[x,y]
            n_x, n_y = get_random_neumann_neighborhood(x, y, SPACE_SIZE)
            n_p = particles[n_x, n_y]
            mobility_factor = np.sqrt(MOBILITY_FACTOR[p['type']] * MOBILITY_FACTOR[n_p['type']])
            if not moved[x, y] and not moved[n_x, n_y] and \
               len(p['bonds']) == 0 and len(n_p['bonds']) == 0 and \
               evaluate_probability(mobility_factor):
                    particles[x,y], particles[n_x,n_y] = n_p, p
                    moved[x, y] = moved[n_x, n_y] = True

After selecting one cell ([x, y]), get_random_neumann_neighborhood (x, y, SPACE_SIZE) randomly selects the cell to move to.

There is a variable called moved on the first line, what is this? In fact, particles did not store None, but moved stores False.

moved = np.full(particles.shape, False, dtype=bool)
moved
# => array([[False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False, False, False,
        False, False, False, False, False, False, False]])

moved looks to see if the selected cell has already been moved. By making the rule that "a cell can be moved only once in one loop", it is assumed that the change of the cell is made minute. Moved cells are set to True, and cells that have not yet been moved are set to False so that they can be referenced.

In the for statement, the move is set to be performed only when the following conditions are met.

--Is the moved variable of the two cells to be moved False? -Is the molecule to be moved bound to another? --Is the return value of the evaluate_probability function True?

Molecular reaction

Finally, I will explain the main reaction program. A function is created for each reaction as shown below. The function is imported from another file, so let's take a look there.

for x in range(SPACE_SIZE):
        for y in range(SPACE_SIZE):
            production(particles, x, y, PRODUCTION_PROBABILITY)
            disintegration(particles, x, y, DISINTEGRATION_PROBABILITY)
            bonding(particles, x, y, BONDING_CHAIN_INITIATE_PROBABILITY,
                                     BONDING_CHAIN_SPLICE_PROBABILITY,
                                     BONDING_CHAIN_EXTEND_PROBABILITY)
            bond_decay(particles, x, y, BOND_DECAY_PROBABILITY)
            absorption(particles, x, y, ABSORPTION_PROBABILITY)
            emission(particles, x, y, EMISSION_PROBABILITY)

production Production is a reaction in which a catalyst transforms two neighboring substrates into a single membrane. The reaction formula is as follows.

2S + C → L + C

Let's see how this is actually programmed.

def production(particles, x, y, probability):
    p = particles[x,y]
    #Randomly select two nearby particles of interest
    n0_x, n0_y, n1_x, n1_y = get_random_2_moore_neighborhood(x, y, particles.shape[0])
    n0_p = particles[n0_x, n0_y]
    n1_p = particles[n1_x, n1_y]
    if p['type'] != 'CATALYST' or n0_p['type'] != 'SUBSTRATE' or n1_p['type'] != 'SUBSTRATE':
        return
    if evaluate_probability(probability):
        n0_p['type'] = 'HOLE'
        n1_p['type'] = 'LINK'

Randomly select two particles from the neighboring particles of interest, and empty one substrate molecule and the other substrate molecule with a certain probability (= ʻevaluate_probability (probability) `) only under the following conditions ..

--The particles you are currently looking at are catalyst molecules --Two randomly selected neighboring particles are substrate molecules

By the way, the get_random_2_moore_neighborhood method that randomly selects two neighboring particles is as follows.

def get_random_2_moore_neighborhood(x, y, space_size):
    n0_x, n0_y = get_random_moore_neighborhood(x, y, space_size)
    if x == n0_x:
        n1_x = np.random.choice([(n0_x+1)%space_size, (n0_x-1)%space_size])
        n1_y = n0_y
    elif y == n0_y:
        n1_x = n0_y
        n1_y = np.random.choice([(n0_y+1)%space_size, (n0_y-1)%space_size])
    else:
        n= [(x, n0_y), (n0_x, y)]
        n1_x, n1_y = n[np.random.randint(len(n))]
    return n0_x, n0_y, n1_x, n1_y
def get_random_moore_neighborhood(x, y, space_size):
    neighborhood = get_moore_neighborhood(x, y, space_size)
    nx, ny = neighborhood[np.random.randint(len(neighborhood))]
    return nx, ny

disintegration Disintegration is a reaction in which membrane molecules disintegrate back into two substrates. Membrane molecules collapse with a certain probability, but they may not collapse immediately depending on the surrounding conditions. Specifically, the situation is as follows.

--There is no space around to release the coexisting substrate molecules --There is no space around to accommodate the two split substrate molecules.

Therefore, if it is evaluated that the membrane molecule collapses with a certain probability, set disintegrating_flag to True. No collapse occurs when disintegrating_flag is False. As a result, even if the membrane molecules do not collapse in the surrounding conditions, they will try to collapse again in the next and subsequent reactions.

def disintegration(particles, x, y, probability):
    p = particles[x,y]
    #Disintegration may not happen immediately, so flag it once
    if p['type'] in ('LINK', 'LINK_SUBSTRATE') and evaluate_probability(probability):
        p['disintegrating_flag'] = True

    if not p['disintegrating_flag']:
        return
    #If LINK contains SUBSTRATE, execute emission with probability 1 to force emission
    emission(particles, x, y, 1.0)
    #Randomly select particles in the vicinity of the target
    n_x, n_y = get_random_moore_neighborhood(x, y, particles.shape[0])
    n_p = particles[n_x, n_y]
    if p['type'] == 'LINK' and n_p['type'] == 'HOLE':
        #Bond to eliminate all LINK interconnects_Execute decay with probability 1
        bond_decay(particles, x, y, 1.0)
        # disintegration
        p['type']   = 'SUBSTRATE'
        n_p['type'] = 'SUBSTRATE'
        p['disintegrating_flag'] = False

A specific explanation of bond_decay and ʻemission` will be given later.

bonding Bonding is a reaction in which a membrane molecule binds to a nearby membrane molecule. This program is longer than the others, so I'll show you the whole thing first.

def bonding(particles, x, y,
            chain_initiate_probability, chain_splice_probability, chain_extend_probability,
            chain_inhibit_bond_flag=True, catalyst_inhibit_bond_flag=True):
    p = particles[x,y]
    #Randomly select particles in the vicinity of the target
    n_x, n_y = get_random_moore_neighborhood(x, y, particles.shape[0])
    #Check the type of two molecules, the number of bonds, the angle, and the intersection
    n_p = particles[n_x, n_y]
    if not p['type'] in ('LINK', 'LINK_SUBSTRATE'):
        return
    if not n_p['type'] in ('LINK', 'LINK_SUBSTRATE'):
        return
    if (n_x, n_y) in p['bonds']:
        return
    if len(p['bonds']) >= 2 or len(n_p['bonds']) >= 2:
        return
    an0_x, an0_y, an1_x, an1_y = get_adjacent_moore_neighborhood(x, y, n_x, n_y, particles.shape[0])
    if (an0_x, an0_y) in p['bonds'] or (an1_x, an1_y) in p['bonds']:
        return
    an0_x, an0_y, an1_x, an1_y = get_adjacent_moore_neighborhood(n_x, n_y, x, y, particles.shape[0])
    if (an0_x, an0_y) in n_p['bonds'] or (an1_x, an1_y) in n_p['bonds']:
        return
    an0_x, an0_y, an1_x, an1_y = get_adjacent_moore_neighborhood(x, y, n_x, n_y, particles.shape[0])
    if (an0_x, an0_y) in particles[an1_x,an1_y]['bonds']:
        return
    #Bonding does not occur in the following two cases
    # 1)When there is a chain of membranes near moore(chain_inhibit_bond_flag)
    # 2)When a catalyst molecule is present near moore(catalyst_inhibit_bond_flag)
    mn_list = get_moore_neighborhood(x, y, particles.shape[0]) + get_moore_neighborhood(n_x, n_y, particles.shape[0])
    if catalyst_inhibit_bond_flag:
        for mn_x, mn_y in mn_list:
            if particles[mn_x,mn_y]['type'] is 'CATALYST':
                return
    if chain_inhibit_bond_flag:
        for mn_x, mn_y in mn_list:
            if len(particles[mn_x,mn_y]['bonds']) >= 2:
                if not (x, y) in particles[mn_x,mn_y]['bonds'] and not (n_x, n_y) in particles[mn_x,mn_y]['bonds']:
                    return
    # Bonding
    if len(p['bonds'])==0 and len(n_p['bonds'])==0:
        prob = chain_initiate_probability
    elif len(p['bonds'])==1 and len(n_p['bonds'])==1:
        prob = chain_splice_probability
    else:
        prob = chain_extend_probability
    if evaluate_probability(prob):
        p['bonds'].append((n_x, n_y))
        n_p['bonds'].append((x, y))

Before looking at it in detail, there are some conditions for the binding of membrane molecules, so let's check those conditions first.

--Up to 2 bonds can form a membrane molecule --The angle between the two bonds is 90 degrees or more (45 degrees is prohibited) --Crossing connections are prohibited --Use different binding probabilities for the two membrane molecules in the following three cases: --When two membrane molecules do not have a bond (chain_initiate_probability argument) --If one membrane molecule already has a bond (chain_extend_probability argument) --If two membrane molecules already have a bond (chain_extend_probability argument)

In addition, the following restrictions have been added to facilitate the formation of the film.

--Limited by combined chain --Cannot bond if there are two bonded membrane molecules near Moore (controlled by chain_inhibit_bond_flag) --Suppression by catalytic molecules --Cannot bind if catalyst molecules are present near Moore (controlled by catalyst_inhibit_bond_flag)

When I first read this, I was very wondering. Why is it so easy to form a membrane when it limits or suppresses the binding of membrane molecules?

bond_decay This is a reaction in which the bonds of membrane molecules disappear with a certain probability due to the reverse reaction of bonding. The fun of autopoiesis is that the membrane is maintained while formation and disappearance occur with a certain probability.

The implementation is as follows. If the particle of interest is a membrane molecule, there is a certain probability that the information will be erased from the bonds of that cell and the cell to which it is bound.

def bond_decay(particles, x, y, probability):
    p = particles[x,y]
    if p['type'] in ('LINK', 'LINK_SUBSTRATE') and evaluate_probability(probability):
        for b in p['bonds']:
            particles[b[0], b[1]]['bonds'].remove((x, y))
        p['bonds'] = []

absorption, emission Membrane molecules are permeable to substrate molecules. This movement is achieved in the SCL model by the membrane absorbing and releasing adjacent substrates.

Recommended Posts

Completely understood Chapter 3 of "Making and Moving ALife"
Explanation and implementation of PRML Chapter 4
[Python] Chapter 02-01 Basics of Python programs (operations and variables)
[Python of Hikari-] Chapter 06-02 Function (argument and return value 1)
[Python] Chapter 01-02 About Python (Execution and installation of development environment)