This tutorial is a static non-editable version. You can launch an
interactive, editable version without installing any local files
using the Binder service (although note that at some times this
may be slow or fail to open):

Once you have some neurons, the next step is to connect them up via
synapses. We’ll start out with doing the simplest possible type of
synapse that causes an instantaneous change in a variable after a spike.

There are a few things going on here. First of all, let’s recap what is
going on with the NeuronGroup. We’ve created two neurons, each of
which has the same differential equation but different values for
parameters I and tau. Neuron 0 has I=2 and tau=10*ms which means
that is driven to repeatedly spike at a fairly high rate. Neuron 1 has
I=0 and tau=100*ms which means that on its own - without the
synapses - it won’t spike at all (the driving current I is 0). You can
prove this to yourself by commenting out the two lines that define the
synapse.

Next we define the synapses: Synapses(source,target,...) means
that we are defining a synaptic model that goes from source to
target. In this case, the source and target are both the same, the
group G. The syntax on_pre='v_post+=0.2' means that when a
spike occurs in the presynaptic neuron (hence on_pre) it causes an
instantaneous change to happen v_post+=0.2. The _post means
that the value of v referred to is the post-synaptic value, and it
is increased by 0.2. So in total, what this model says is that whenever
two neurons in G are connected by a synapse, when the source neuron
fires a spike the target neuron will have its value of v increased
by 0.2.

However, at this point we have only defined the synapse model, we
haven’t actually created any synapses. The next line
S.connect(i=0,j=1) creates a synapse from neuron 0 to neuron 1.

In the previous section, we hard coded the weight of the synapse to be
the value 0.2, but often we would to allow this to be different for
different synapses. We do that by introducing synapse equations.

This example behaves very similarly to the previous example, but now
there’s a synaptic weight variable w. The string 'w:1' is an
equation string, precisely the same as for neurons, that defines a
single dimensionless parameter w. We changed the behaviour on a
spike to on_pre='v_post+=w' now, so that each synapse can behave
differently depending on the value of w. To illustrate this, we’ve
made a third neuron which behaves precisely the same as the second
neuron, and connected neuron 0 to both neurons 1 and 2. We’ve also set
the weights via S.w='j*0.2'. When i and j occur in the
context of synapses, i refers to the source neuron index, and j
to the target neuron index. So this will give a synaptic connection from
0 to 1 with weight 0.2=0.2*1 and from 0 to 2 with weight
0.4=0.2*2.

Here we’ve created a dummy neuron group of N neurons and a dummy
synapses model that doens’t actually do anything just to demonstrate the
connectivity. The line S.connect(condition='i!=j',p=0.2) will
connect all pairs of neurons i and j with probability 0.2 as
long as the condition i!=j holds. So, how can we see that
connectivity? Here’s a little function that will let us visualise it.

There are two plots here. On the left hand side, you see a vertical line
of circles indicating source neurons on the left, and a vertical line
indicating target neurons on the right, and a line between two neurons
that have a synapse. On the right hand side is another way of
visualising the same thing. Here each black dot is a synapse, with x
value the source neuron index, and y value the target neuron index.

Let’s see how these figures change as we change the probability of a
connection:

And let’s see what another connectivity condition looks like. This one
will only connect neighbouring neurons.

start_scope()N=10G=NeuronGroup(N,'v:1')S=Synapses(G,G)S.connect(condition='abs(i-j)<4 and i!=j')visualise_connectivity(S)

Try using that cell to see how other connectivity conditions look like.

You can also use the generator syntax to create connections like this
more efficiently. In small examples like this, it doesn’t matter, but
for large numbers of neurons it can be much more efficient to specify
directly which neurons should be connected than to specify just a
condition. Note that the following example uses skip_if_invalid to
avoid errors at the boundaries (e.g. do not try to connect the neuron
with index 1 to a neuron with index -2).

start_scope()N=10G=NeuronGroup(N,'v:1')S=Synapses(G,G)S.connect(j='k for k in range(i-3, i+4) if i!=k',skip_if_invalid=True)visualise_connectivity(S)

If each source neuron is connected to precisely one target neuron (which
would be normally used with two separate groups of the same size, not
with identical source and target groups as in this example), there is a
special syntax that is extremely efficient. For example, 1-to-1
connectivity looks like this:

You can also do things like specifying the value of weights with a
string. Let’s see an example where we assign each neuron a spatial
location and have a distance-dependent connectivity function. We
visualise the weight of a synapse by the size of the marker.

Brian’s synapse framework is very general and can do things like
short-term plasticity (STP) or spike-timing dependent plasticity (STDP).
Let’s see how that works for STDP.

STDP is normally defined by an equation something like this:

\[\Delta w = \sum_{t_{pre}} \sum_{t_{post}} W(t_{post}-t_{pre})\]

That is, the change in synaptic weight w is the sum over all presynaptic
spike times \(t_{pre}\) and postsynaptic spike times
\(t_{post}\) of some function \(W\) of the difference in these
spike times. A commonly used function \(W\) is:

Simulating it directly using this equation though would be very
inefficient, because we would have to sum over all pairs of spikes. That
would also be physiologically unrealistic because the neuron cannot
remember all its previous spike times. It turns out there is a more
efficient and physiologically more plausible way to get the same effect.

We define two new variables \(a_{pre}\) and \(a_{post}\) which
are “traces” of pre- and post-synaptic activity, governed by the
differential equations:

To see that this formulation is equivalent, you just have to check that
the equations sum linearly, and consider two cases: what happens if the
presynaptic spike occurs before the postsynaptic spike, and vice versa.
Try drawing a picture of it.

Now that we have a formulation that relies only on differential
equations and spike events, we can turn that into Brian code.

There are a few things to see there. Firstly, when defining the synapses
we’ve given a more complicated multi-line string defining three synaptic
variables (w, apre and apost). We’ve also got a new bit of
syntax there, (event-driven) after the definitions of apre and
apost. What this means is that although these two variables evolve
continuously over time, Brian should only update them at the time of an
event (a spike). This is because we don’t need the values of apre
and apost except at spike times, and it is more efficient to only
update them when needed.

Next we have a on_pre=... argument. The first line is
v_post+=w: this is the line that actually applies the synaptic
weight to the target neuron. The second line is apre+=Apre which
encodes the rule above. In the third line, we’re also encoding the rule
above but we’ve added one extra feature: we’ve clamped the synaptic
weights between a minimum of 0 and a maximum of wmax so that the
weights can’t get too large or negative. The function
clip(x,low,high) does this.

Finally, we have a on_post=... argument. This gives the statements
to calculate when a post-synaptic neuron fires. Note that we do not
modify v in this case, only the synaptic variables.

Now let’s see how all the variables behave when a presynaptic spike
arrives some time before a postsynaptic spike.