Build Synapses

When you have some neurons, you need to build synapses to connect them. This tutorial will show you how to use synapse models to connect neurons to networks.

As neuron models, the definition and usage of the synapse model are separated from each other. Specifically, two classes should be used:

  • brainpy.SynType: Define the abstract synapse model.

  • brainpy.SynConn: Use the abstract synapse model to generate a concrete synapse connection.

We will first take a look at the definition with brainpy.SynType, then in the second part, we will show the usage with brainpy.SynConn.

Before we start, let’s import the BrainPy and Numpy packages.

[1]:
import brainpy as bp
import numpy as np

brainpy.SynType

You can define any abstract synapse type with SynType, which is very flexible.

As neuron models, four parameters should be specified to initialize a SynType:

  • name: The synapse model name.

  • steps: The step functions to update at each time step. You can define your own update logic functions.

  • requires: The data required to run this synapse model, such as synaptic states and neuronal states of the connected neurons.

  • mode: Whether define the model based on scalar, vector, or matrix.

We provide a data structure brainpy.types.SynState to support the synapse state management.

Three kinds of definition provided in BrainPy to define a SynType:

  • mode = 'scalar': Synapse state ST represents the state of a single synapse connection. And, each item in ST is a scalar.

  • mode = 'vector': Synapse state ST represents the state of a group of synapse connections. And each item in ST is a vector,

  • mode = 'matrix': Synapse state ST represents the state of a group of synapse connections. And each item in ST is a matrix with the shape of (num_pre, num_post).

The definition logic of scalar-based models may be more straightforward than vector- and matrix- based models. We will first introduce the definition of a simple synapse model in scalar-based mode.

Example: AMPA synapse model (scalar mode)

Let’s first take the AMPA synapse model as an example to see how to define a SynType in BrainPy.

The formal equations of an AMPA synapse is given by:

\[I_{syn}= \bar{g}_{syn} s (V-E_{syn})\]
\[\frac{d s}{d t}=-\frac{s}{\tau_{decay}}+\sum_{k} \delta(t-t_{j}^{k})\]

where \(\bar{g}_{syn}\) is the maximum synaptic conductance, \(s\) is the gating variable, and \(V\) is the membrane potential of the postsynaptic neuron. The time constant \(\tau_{decay}\) is about 2ms and the equilibrium potential \(E_{syn}\) for AMPA synapse is 0.

[2]:
# parameters we need #
# ------------------ #

tau_decay = 2.   # time constant of the dacay after synapse respond to a neurontransmitter.
g_max = .10      # Voltage-controlled conductance per unit area
                 # associated with the Sodium (Na) and Potassium (K) ion-channels on the synapse (postsynaptic membrane).
E = 0.           # The equilibrium potentials for the synapse.

Please check Differential equations to see how BrainPy supports differential equations.

[3]:
# dynamics of gating variable
@bp.integrate
def ints(s, t):
    return - s / tau_decay

Here, let’s first define the state of a synapse model.

[4]:
ST=bp.types.SynState(['s'], help='AMPA synapse state.')

In ST, the dynamical variable \(s\) is included.

Since a synapse connects a presynaptic neuron and a postsynaptic neuron, we need to know the state of the two neurons.

[5]:
pre=bp.types.NeuState(['spike'], help='Presynaptic neuron state must have "sp" item.')
post=bp.types.NeuState(['V', 'input'], help='Postsynaptic neuron state must have "V" and "inp" item.')

From the equations of the AMPA synapse, we need to know whether the presynaptic neuron (pre) provides a \(spike\) at current time. We also need to know the current membrane potential \(V\) of the postsynaptic neuron, and add synaptic current to the \(input\) item of the post.

Based on the synapse state ST and neuron states, the update logic of the synapse model from the current time point (\(t\)) to the next time point \((t + dt)\) can be defined as:

[6]:
def update(ST, _t, pre):
    s = ints(ST['s'], _t)
    if pre['spike'] == True:
        s += 1
    ST['s'] = s

In this example, the update() function of AMPA model needs three data:

  • ST: The synapse state.

  • _t: The system time at current point.

  • pre: The neuron state of the presynaptic neuron.

We also need to define an output logic to compute the synaptic current and add it to the postsynaptic inputs.

The synaptic delay between a presynaptic spike and the postsynaptic change can be implemented with a @bp.delayed decorator.

[7]:
@bp.delayed
def output(ST, post):
    I_syn = - g_max * ST['s'] * (post['V'] - E)
    post['input'] += I_syn

Putting together, an AMPA synapse model is defined as:

[8]:
AMPA = bp.SynType(name='AMPA_synapse',
                  ST=ST,
                  requires=dict(pre=pre, post=post),
                  steps=(update, output),
                  mode='scalar')

Here, we should note that we just define an abstract AMPA synapse model. This model can run with any number of synapse connections. Only after defining a concrete synapse connection, can we use it to construct a network.

Example: AMPA synapse model (matrix mode)

In matrix mode, each item in the synapse state ST is a matrix.

The differential equation part is the same as the scalar mode, and we also need a SynState and the NeuState of presynaptic and postsynaptic neurons.

[9]:
tau_decay = 2.
g_max = .10
E = 0.

@bp.integrate
def ints(s, t):
    return - s / tau_decay

ST=bp.types.SynState(['s', 'g'], help='AMPA synapse state.')
pre=bp.types.NeuState(['spike'], help='Presynaptic neuron state must have "sp" item.')
post=bp.types.NeuState(['V', 'input'], help='Presynaptic neuron state must have "V" and "inp" item.')

We also need to define a connectivity matrix to specify the connectivity patterns between the presynaptic neurons and postsynaptic neurons, which can be defined with brainpy.types.MatConn().

[10]:
conn_mat=bp.types.MatConn()

Here is an example of the connectivity matrix:

2c79c76d5f5a4ca8ad9e328bf4793464

The update and output are also similar to the scalar mode, but notice that the pre and post here are vectors, so all the operations are vectors.

[11]:
def update(ST, _t, pre, conn_mat):
    s = ints(ST['s'], _t)
    s += pre['spike'].reshape((-1, 1)) * conn_mat
    ST['s'] = s
    ST['g'] = g_max * s

@bp.delayed
def output(ST, post):
    g = np.sum(ST['g'], axis=0)
    post['input'] -= g * (post['V'] - E)

AMPA = bp.SynType(name='AMPA_synapse',
                  ST=ST,
                  requires=dict(pre=pre, post=post, conn_mat=conn_mat),
                  steps=(update, output),
                  mode='matrix')

Vector mode

In vector mode, each item in the synapse state ST is a vector.

Let’s look at the synaptic connections in vector form.

Synaptic connectivity

Suppose we have two vectors of neurons and a vector of synapses connecting the neurons within the two neuron vectors. Many different connectivities are possible, and we use \(index\) to recognize different synapses.

Each synapse receives information from one presynaptic neuron, and, commonly, different synapses get inconsistent signals. Therefore, it is helpful to specify a map from the presynaptic neuron vector to the synapses vector.

For example, we have a connectivity as below:

06a74eaa7c944727939f6c2436a81357

Where 1 and 0 indicate the presence and absence of synaptic connections, respectively. We can then arrange the synapses in the following manner:

6e1b9a7c00834abfa8706918dd82f6ce

We can create a pre2syn list, the indexes of this list correspond to the indexes of the presynaptic neurons vector, and the elements indicate the indexes of synapses that having connections to the neuron. Here, the first neuron connects the 3rd, 5th, and 7th neurons with synapses 0, 1, 2, so we store [0, 1, 2] as the first element of the pre2syn list. Thus, if the first neuron fire, then we can get the indexes of synapses by syn_ids = pre2syn[0] and changes the states of those synapses.

abba5b97acd84c90bd6286bda0c0842a

Similarly, we can use a post2syn list to indicate the connections between synapses and postsynaptic neurons. The indexes of this list correspond to the indexes of the presynaptic neurons vector, and the elements indicate the indexes of synapses that having connections to the neuron.

ab8348cf91804b19b417969a175cbf88

We can also create a map between two neurons vectors using a pre2post list and a post2pre list.

25e83055b6794099b7dbcfd6a52e58d4

679d53d9ba464d158dcaa3c953ac965d

Other mapping ways are also possible.

e68ba80fb1e4435da0cf08170ab60720

197f16d3f4544e33b78ea7ee37f96808

Example: AMPA synapse model (vector mode)

Now let’s see how to implement a vector-based synapses model by taking AMPA model as example. The formal equations of an AMPA synapse is the same as the scalar-based one:

\[I_{syn}= \bar{g}_{syn} s (V-E_{syn})\]
\[\frac{d s}{d t}=-\frac{s}{\tau_{decay}}+\sum_{k} \delta(t-t_{j}^{k})\]

where \(\bar{g}_{syn}\) is the maximum synaptic conductance, \(s\) is the gating variable, and \(V\) is the membrane potential of the postsynaptic neuron. The time constant \(\tau_{decay}\) is about 2ms and the equilibrium potential \(E_{syn}\) for AMPA synapse is 0.

The differential equation part is the same as the scalar and matrix mode, and we also need a SynState and the NeuState of presynaptic and postsynaptic neurons.

[12]:
tau_decay = 2.
g_max = .10
E = 0.

@bp.integrate
def ints(s, t):
    return - s / tau_decay

ST=bp.types.SynState(['s'], help='AMPA synapse state.')
pre=bp.types.NeuState(['spike'], help='Presynaptic neuron state must have "sp" item.')
post=bp.types.NeuState(['V', 'input'], help='Presynaptic neuron state must have "V" and "inp" item.')

For the mapping between synapse and neurons, BrainPy provides brainpy.types.ListConn.

[13]:
pre2syn = bp.types.ListConn()
post2syn = bp.types.ListConn()

Assume the items in the synapse state ST and neuron states pre and post are vectors, and we have the mapping lists pre2syn and post2syn, the update logic of vector-based AMPA synapse model is:

[14]:
def update(ST, _t, pre, pre2syn):
    s = ints(ST['s'], _t)

    spikeike_idx = np.where(pre['spike'] > 0.)[0]
    for i in spikeike_idx:
        syn_idx = pre2syn[i]
        s[syn_idx] += 1.

    # update values
    ST['s'] = s


@bp.delayed
def output(ST, post, post2syn):
    post_cond = np.zeros(len(post2syn), dtype=np.float_)
    for post_id, syn_ids in enumerate(post2syn):
        post_cond[post_id] = np.sum(g_max * ST['s'][syn_ids])
    post['input'] -= post_cond * (post['V'] - E)

AMPA_vector = bp.SynType(name='AMPA_synapse',
                         ST=ST,
                         requires=dict(pre=pre, post=post,
                                       pre2syn=pre2syn, post2syn=post2syn),
                         steps=(update, output),
                         mode='vector')

brainpy.SynConn

Synapse connections determine the architecture of a network. A brainpy.SynConn receives the following parameters:

  • model: The synapse type will be used to generate a synapse connection.

  • pre_group: The presynaptic neuron group.

  • post_group: The postsynaptic neuron group.

  • conn: The connection method to create synaptic connectivity between the neuron groups.

  • monitors: The items to monitor (record the history values.)

  • delay: The time of the synapse delay (in milliseconds).

BrainPy pre-defines several commonly used connection methods in brainpy.connect, read Usage of connect module for more details.

Let’s take our defined AMPA model as an exmaple.

We can get pre-defined neuron models from the bpmodels package. Here we use the leaky intergrate-and-fire (LIF) model to create neuron groups

[15]:
from bpmodels.neurons import get_LIF

LIF = get_LIF(V_rest=-65., V_reset=-65., V_th=-55.)
pre = bp.NeuGroup(LIF, 1, monitors=['spike', 'V'])
pre.ST['V'] = -65.
post = bp.NeuGroup(LIF, 1, monitors=['V'])
post.ST['V'] = -65.
[16]:
syn = bp.SynConn(model=AMPA, pre_group=pre, post_group=post,
                  conn=bp.connect.All2All(),
                  monitors=['s'], delay=1.5)

You can specify the synapse behavior by using syn.runner.set_schedule.

[17]:
syn.runner.set_schedule(['input', 'update', 'output', 'monitor'])

Note that you cannot run the synapse connection (unlike neuron groups). You have to run them in a network.

[18]:
net = bp.Network(pre, syn, post)

net.run(duration=100., inputs=(pre, "ST.input", 20.))