Synapses (Brian 1 –> 2 conversion)
Converting Brian 1’s Connection
class
In Brian 2, the Synapses
class is the only class to model synaptic
connections, you will therefore have to convert all uses of Brian 1’s
Connection
class. The Connection
class increases a post-synaptic
variable by a certain amount (the “synaptic weight”) each time a pre-synaptic
spike arrives. This has to be explicitly specified when using the Synapses
class, the equivalent to the basic Connection
usage is:
Brian 1 |
Brian 2 |
---|---|
conn = Connection(source, target, 'ge')
|
conn = Synapses(source, target, 'w : siemens',
on_pre='ge += w')
|
Note that he variable w
, which stores the synaptic weight, has to have the
same units as the post-synaptic variable (in this case: ge
) that it
increases.
Creating synapses and setting weights
With the Connection
class, creating a synapse and setting its weight is a
single process whereas with the Synapses
class those two steps are separate.
There is no direct equivalent to the convenience functions connect_full
,
connect_random
and connect_one_to_one
, but you can easily implement
the same functionality with the general mechanism of Synapses.connect
:
Brian 1 |
Brian 2 |
---|---|
conn1 = Connection(source, target, 'ge')
conn1[3, 5] = 3*nS
|
conn1 = Synapses(source, target, 'w: siemens',
on_pre='ge += w')
conn1.connect(i=3, j=5)
conn1.w[3, 5] = 3*nS # (or conn1.w = 3*nS)
|
conn2 = Connection(source, target, 'ge')
conn2.connect_full(source, target, 5*nS)
|
conn2 = ... # see above
conn2.connect()
conn2.w = 5*nS
|
conn3 = Connection(source, target, 'ge')
conn3.connect_random(source, target,
sparseness=0.02,
weight=2*ns)
|
conn3 = ... # see above
conn3.connect(p=0.02)
conn3.w = 2*nS
|
conn4 = Connection(source, target, 'ge')
conn4.connect_one_to_one(source, target,
weight=4*nS)
|
conn4 = ... # see above
conn4.connect(j='i')
conn4.w = 4*nS
|
conn5 = IdentityConnection(source, target,
weight=3*nS)
|
conn5 = Synapses(source, target,
'w : siemens (shared)')
conn5.w = 3*nS
|
Weight matrices
Brian 2’s Synapses
class does not support setting the weights of a neuron with
a weight matrix. However, Synapses.connect
creates the synapses in a
predictable order (first all synapses for the first pre-synaptic cell, then all
synapses for the second pre-synaptic cell, etc.), so a reshaped “flat” weight
matrix can be used:
Brian 1 |
Brian 2 |
---|---|
# len(source) == 20, len(target) == 30
conn6 = Connection(source, target, 'ge')
W = rand(20, 30)*nS
conn6.connect(source, target, weight=W)
|
# len(source) == 20, len(target) == 30
conn6 = Synapses(source, target, 'w: siemens',
on_pre='ge += w')
W = rand(20, 30)*nS
conn6.connect()
conn6.w = W.flatten()
|
However note that if your weight matrix can be described mathematically (e.g.
random as in the example above), then you should not create a weight matrix in
the first place but use Brian 2’s mechanism to set variables based on
mathematical expressions (in the above case: conn5.w = 'rand()'
). Especially
for big connection matrices this will have better performance, since it will be
executed in generated code. You should only resort to explicit weight matrices
when there is no alternative (e.g. to load weights from previous simulations).
In Brian 1, you can restrict the functions connect
, connect_random
, etc.
to subgroups. Again, there is no direct equivalent to this in Brian 2, but the
general string syntax allows you to make connections conditional on logical
statements that refer to pre-/post-synaptic indices and can therefore also used
to restrict the connection to a subgroup of cells. When you set the synaptic
weights, you can however use subgroups to restrict the subset of weights you
want to set.
Brian 1 |
Brian 2 |
---|---|
conn7 = Connection(source, target, 'ge')
conn7.connect_full(source[:5], target[5:10], 5*nS)
|
conn7 = Synapses(source, target, 'w: siemens',
on_pre='ge += w')
conn7.connect('i < 5 and j >=5 and j <10')
# Alternative (more efficient):
# conn7.connect(j='k in range(5, 10) if i < 5')
conn7.w[source[:5], target[5:10]] = 5*nS
|
Connections defined by functions
Brian 1 allowed you to pass in a function as the value for the weight
argument in a connect
call (and also for the sparseness argument in
connect_random
). You should be able to replace such use cases by the the
general, string-expression based method:
Brian 1 |
Brian 2 |
---|---|
conn8 = Connection(source, target, 'ge')
conn8.connect_full(source, target,
weight=lambda i,j:(1+cos(i-j))*2*nS)
|
conn8 = Synapses(source, target, 'w: siemens',
on_pre='ge += w')
conn8.connect()
conn8.w = '(1 + cos(i - j))*2*nS'
|
conn9 = Connection(source, target, 'ge')
conn9.connect_random(source, target,
sparseness=0.02,
weight=lambda:rand()*nS)
|
conn9 = ... # see above
conn9.connect(p=0.02)
conn9.w = 'rand()*nS'
|
conn10 = Connection(source, target, 'ge')
conn10.connect_random(source, target,
sparseness=lambda i,j:exp(-abs(i-j)*.1),
weight=2*ns)
|
conn10 = ... # see above
conn10.connect(p='exp(-abs(i - j)*.1)')
conn10.w = 2*nS
|
Delays
The specification of delays changed in several aspects from Brian 1 to Brian 2:
In Brian 1, delays where homogeneous by default, and heterogeneous delays had
to be marked by delay=True
, together with the specification of the maximum
delay. In Brian 2, heterogeneous delays are the default and you do not have to
state the maximum delay. Brian 1’s syntax of specifying a pair of values to get
randomly distributed delays in that range is no longer supported, instead use
Brian 2’s standard string syntax:
Brian 1 |
Brian 2 |
---|---|
conn11 = Connection(source, target, 'ge', delay=True,
max_delay=5*ms)
conn11.connect_full(source, target, weight=3*nS,
delay=(0*ms, 5*ms))
|
conn11 = Synapses(source, target, 'w : siemens',
on_pre='ge += w')
conn11.connect()
conn11.w = 3*nS
conn11.delay = 'rand()*5*ms'
|
Modulation
In Brian 2, there’s no need for the modulation
keyword that Brian 1 offered,
you can describe the modulation as part of the on_pre
action:
Brian 1 |
Brian 2 |
---|---|
conn12 = Connection(source, target, 'ge',
modulation='u')
|
conn12 = Synapses(source, target, 'w : siemens',
on_pre='ge += w * u_pre')
|
Structure
There’s no equivalen for Brian 1’s structure
keyword in Brian 2, synapses
are always stored in a sparse data structure. There is currently no support for
changing synapses at run time (i.e. the “dynamic” structure of Brian 1).
Converting Brian 1’s Synapses
class
Brian 2’s Synapses
class works for the most part like the class of the same
name in Brian 1. There are however some differences in details, listed below:
Synaptic models
The basic syntax to define a synaptic model is unchanged, but the keywords
pre
and post
have been renamed to on_pre
and on_post
,
respectively.
Brian 1 |
Brian 2 |
---|---|
stdp_syn = Synapses(inputs, neurons, model='''
w:1
dApre/dt = -Apre/taupre : 1 (event-driven)
dApost/dt = -Apost/taupost : 1 (event-driven)''',
pre='''ge + =w
Apre += delta_Apre
w = clip(w + Apost, 0, gmax)''',
post='''Apost += delta_Apost
w = clip(w + Apre, 0, gmax)''')
|
stdp_syn = Synapses(inputs, neurons, model='''
w:1
dApre/dt = -Apre/taupre : 1 (event-driven)
dApost/dt = -Apost/taupost : 1 (event-driven)''',
on_pre='''ge + =w
Apre += delta_Apre
w = clip(w + Apost, 0, gmax)''',
on_post='''Apost += delta_Apost
w = clip(w + Apre, 0, gmax)''')
|
Lumped variables (summed variables)
The syntax to define lumped variables (we use the term “summed variables” in
Brian 2) has been changed: instead of assigning the synaptic variable to the
neuronal variable you’ll have to include the summed variable in the synaptic
equations with the flag (summed)
:
Brian 1 |
Brian 2 |
---|---|
# a non-linear synapse (e.g. NMDA)
neurons = NeuronGroup(1, model='''
dv/dt = (gtot - v)/(10*ms) : 1
gtot : 1''')
syn = Synapses(inputs, neurons,
model='''
dg/dt = -a*g+b*x*(1-g) : 1
dx/dt = -c*x : 1
w : 1 # synaptic weight''',
pre='x += w')
neurons.gtot=S.g
|
# a non-linear synapse (e.g. NMDA)
neurons = NeuronGroup(1, model='''
dv/dt = (gtot - v)/(10*ms) : 1
gtot : 1''')
syn = Synapses(inputs, neurons,
model='''
dg/dt = -a*g+b*x*(1-g) : 1
dx/dt = -c*x : 1
w : 1 # synaptic weight
gtot_post = g : 1 (summed)''',
on_pre='x += w')
|
Creating synapses
In Brian 1, synapses were created by assigning True
or an integer (the
number of synapses) to an indexed Synapses
object. In Brian 2, all synapse
creation goes through the Synapses.connect
function. For examples how to
create more complex connection patterns, see the section on translating
Connections
objects above.
Brian 1 |
Brian 2 |
---|---|
syn = Synapses(...)
# single synapse
syn[3, 5] = True
|
syn = Synapses(...)
# single synapse
syn.connect(i=3, j=5)
|
# all-to-all connections
syn[:, :] = True
|
# all-to-all connections
syn.connect()
|
# all to neuron number 1
syn[:, 1] = True
|
# all to neuron number 1
syn.connect(j='1')
|
# multiple synapses
syn[4, 7] = 3
|
# multiple synapses
syn.connect(i=4, j=7, n=3)
|
# connection probability 2%
syn[:, :] = 0.02
|
# connection probability 2%
syn.connect(p=0.02)
|
Multiple pathways
As Brian 1, Brian 2 supports multiple pre- or post-synaptic pathways, with
separate pre-/post-codes and delays. In Brian 1, you have to specify the
pathways as tuples and can then later access them individually by using their
index. In Brian 2, you specify the pathways as a dictionary, i.e. by giving
them individual names which you can then later use to access them (the default
pathways are called pre
and post
):
Brian 1 |
Brian 2 |
---|---|
S = Synapses(...,
pre=('ge + =w',
'''w = clip(w + Apost, 0, inf)
Apre += delta_Apre'''),
post='''Apost += delta_Apost
w = clip(w + Apre, 0, inf)''')
S[:, :] = True
S.delay[1][:, :] = 3*ms # delayed trace
|
S = Synapses(...,
pre={'pre_transmission':
'ge += w',
'pre_plasticity':
'''w = clip(w + Apost, 0, inf)
Apre += delta_Apre'''},
post='''Apost += delta_Apost
w = clip(w + Apre, 0, inf)''')
S.connect()
S.pre_plasticity.delay[:, :] = 3*ms # delayed trace
|
Monitoring synaptic variables
Both in Brian 1 and Brian 2, you can record the values of synaptic variables
with a StateMonitor
. You no longer have to call an explicit indexing function,
but you can directly provide an appropriately indexed Synapses
object. You
can now also use the same technique to index the StateMonitor
object to get
the recorded values, see the respective section in the
Synapses documentation for details.
Brian 1 |
Brian 2 |
---|---|
syn = Synapses(...)
# record all synapse targetting neuron 3
indices = syn.synapse_index((slice(None), 3))
mon = StateMonitor(S, 'w', record=indices)
|
syn = Synapses(...)
# record all synapse targetting neuron 3
mon = StateMonitor(S, 'w', record=S[:, 3])
|