Hi @enyaliosss. I finally managed to look into this more closely. Regarding the question of how to change the order of a pathway: you can write something like S.weight_release_pathway.order = 2
to change the order of the weight_release_pathway
from S
. Your code looks pretty good, but I think there is one major flaw that you cannot avoid if you do it this way: since information about the synapse that should be updated is stored in a single variable on the post-synaptic side, you cannot deal with two spikes for the same target neuron that arrive at the same time step. If you want to handle this situation, you have to do deal with all synapses independently, only shared variables like w_pool
can be stored in the post-synaptic cell.
This complexifies things quite a bit, so if you prefer writing source code as suggested by @rth, this is a completely valid option
It is possible with built-in Brian mechanisms, though, by using a group of “relay neurons”. The idea is the following:
Your usual model would look like this:

i.e:
P = NeuronGroup(...) # Or PoissonGroup, etc.
G = NeuronGroup(...)
S = Synapses(P, G, 'w : 1', ...)
With “relay neurons”, it would rather look like this:
I.e. there is one “neuron” representing each of the synapses:
# P and G as before
weight_group = NeuronGroup(len(P) * len(G),
'''pre_idx : integer (constant)
post_idx : integer (constant)
w : 1
is_spiking boolean''',
threshold='is_spiking', reset='is_spiking = False')
weight_group.pre_idx = [0, 0, 1, 1, 2, 2] # could use np.meshgrid
weight_group.post_idx = [0, 1, 0, 1, 0, 1] # could use np.meshgrid
step_1 = Synapses(P, weight_group, on_pre='is_spiking = True')
step_1.connect('i == pre_idx_post')
step_2 = Synapses(weight_group, G, on_pre='v_post += w')
step_2.connect('j == post_idx_post')
Instead of directly transmitting a spike, this transmits the spike to the “synapse-representing neuron”, which will spike and transmit the spike to the target group. This needs a bit of tweaking with the when
and order
parameters to do all this within a single time step, but it is rather straightforward.
What do you gain from this approach? Now you can add a new group of synapses that connect the “synapse-representing neurons” to each other, in case they target the same neuron:
update_synapses = Synapses(weight_group, weight_group, ...)
# Connect if `post_idx` is the same, but don't connect neurons to themselves
update_synapses.connect('post_idx_pre == post_idx_post and i != j')
I am aware that things like post_idx_pre
are not very easy to read (feel free to rename things for more clarity), but hopefully the general idea is clear. The nice thing is that the two weight updates are now straightforward to write. See the full code for details:
Full code
from brian2 import *
v_th = 15 / 1000
tau = 50 * ms
P = SpikeGeneratorGroup(5, [0, 1, 2, 3, 4, 1], [10*ms, 30*ms, 30*ms, 50*ms, 50*ms, 50*ms],
when='before_thresholds', order=0, name='P')
W_0 = 5
G = NeuronGroup(1, '''dv/dt = -v / tau : 1
w_pool :1 ''', method='exact', name='G')
G.w_pool = W_0
MP = SpikeMonitor(P, record=True)
weight_group = NeuronGroup(len(P)*len(G),
'''w : 1
w_before : 1
pre_idx : integer (constant)
post_idx : integer (constant)
received_spike : boolean
''',
threshold='received_spike', reset='received_spike = False',
name='weight_group')
# All to all connectivity
pre, post = np.meshgrid(np.arange(len(P)), np.arange(len(G)))
weight_group.pre_idx = pre.flatten()
weight_group.post_idx = post.flatten()
# Forward spike to "synapses" and store previous weight
forward = Synapses(P, weight_group, on_pre='w_before = w; received_spike_post = True', name='forward_synapses')
forward.pre.when = 'before_thresholds'
forward.pre.order = 1
forward.connect('i == pre_idx_post')
# Take weight from spike_pool
eta = 0.8
connect_to_neuron = Synapses(weight_group, G,
on_pre={'update_from_pool':
'''
w_update = eta*w_pool_post
w_pre += w_update
w_pool_post -= w_update''',
'forward_spike': 'v_post += w_pre'},
name='connect_to_neuron')
connect_to_neuron.update_from_pool.order = 0
connect_to_neuron.forward_spike.order = 2
connect_to_neuron.connect('j == post_idx_pre')
# Take weight from stronger synapses connecting to same post-synaptic target
update_from_synapses = Synapses(weight_group, weight_group,
on_pre='''delta_w = int(w_post > w_before_pre) * w_post*(w_post - w_before_pre)/(W_0 + w_post)
w_pre += delta_w
w_post -= delta_w''',
name='update_from_synapses')
update_from_synapses.pre.order = 1
# Connect all synapses that target the same neuron, but don't connect neurons to themselves
update_from_synapses.connect('post_idx_pre == post_idx_post and i != j')
MW = StateMonitor(weight_group, 'w', record=True) # StateMonitor for monitoring weights.
MW_POOL = StateMonitor(G, 'w_pool', record=True) # StateMonitor for monitoring w_pool.
G.w_pool = 5
run(100*ms)
print(scheduling_summary())
print(weight_group.w)
print(G.w_pool)
print(MP.t)
print(MP.i)
# Drawing Figure 2.
fig, axs = plt.subplots(7, 1, sharex=True)
colors_styles = [('g', 'solid'), ('r', 'solid'), ('b', 'solid'), ('c', 'dashed'), ('m', 'dashed')]
axs[1].plot(MW_POOL.t / ms, MW_POOL.w_pool[0], color='k', label='POOL')
for idx, (c, style) in enumerate(colors_styles):
# Input spikes
axs[0].plot(MP.t[MP.i == idx]/ms, len(P)-MP.i[MP.i == idx], '^', color=c)
# Weights
axs[2 + idx].plot(MW.t / ms, MW.w[idx], color=c, linestyle=style)
axs[0].axis('off')
axs[0].set_clip_on(False)
axs[6].set_xlabel('Time (ms)')
plt.show()
I tested it with multiple spikes arriving in the same time step here, and the results look reasonable, I’d say:
