Getting the timing right (scheduling 2)
This article is written as a Jupyter notebook which you can execute and modify interactively. You can either download it via the “Source” link on the top right, or run it directly in the browser on the mybinder infrastructure:
For more information, see our general Notes on Notebooks.
Quickstart
To run the code below:- Click on the cell to select it.
- Press
SHIFT+ENTER
on your keyboard or press the play button () in the toolbar above
SHIFT+ENTER
while this cell
is selected.
“Scheduling”: mechanism to determine the order of operations during a simulation
In this video we will look at its importance for:
- propagating synaptic activity
You can also watch the screencast video on Youtube.
from brian2 import *
%matplotlib notebook
plt.rcParams['figure.figsize'] = (4, 3) # reduce figure size for this notebook
prefs.codegen.target = 'numpy'
Let’s say we have a neuron (group
in the following code) that is following some dynamics given by its equations, inputs, etc. Now, we also want to make this neuron spike at a specific time (here, 10ms), e.g. to probe a spike-timing-dependent plasticity mechanism or to model some experimental stimulation.
We could try this by using the approach in the code below: a SpikeGeneratorGroup
spikes at the specified time and connects to the target neuron with a synapse that is strong enough to make the target neuron spike. Will this lead to a target neuron spiking at 10ms?
start_scope()
inp = SpikeGeneratorGroup(1, [0], [10]*ms)
group = NeuronGroup(1, 'dv/dt = -v / (20*ms) : 1',
threshold='v > 1', reset='v = 0', method='exact')
syn = Synapses(inp, group, on_pre='v_post += 2')
syn.connect()
inp_mon = SpikeMonitor(inp)
group_mon = SpikeMonitor(group)
run(20*ms)
fig, ax = plt.subplots()
ax.plot(inp_mon.t/ms, [1], 'o')
ax.plot(group_mon.t/ms, [0], 'o')
ax.set(yticks=[0, 1], yticklabels=['group', 'inp'], xlabel='time (ms)');
As we can see in the above plot, the target neuron spikes one time step (i.e., by default 0.1ms) later than what we intended. The reason for this is the scheduling of the operations:
magic_network.schedule
At the 10ms time step, the SpikeGeneratorGroup
will generate a spike in the thresholds
slot, which then gets propagated to the target neuron in the synapses
slot. This, however, basically ends the time step, the target neuron will only spike in the following time step when our schedule reaches the thresholds
slot again.
To fix this, we might consider to change the order of operations in the schedule. We can do this easily by assigning to the schedule
attribute:
# we swap synapses and thresholds
magic_network.schedule = ['start', 'groups', 'synapses', 'thresholds', 'resets', 'end']
Does this fix our issue? Let’s run the simulation again:
start_scope()
inp = SpikeGeneratorGroup(1, [0], [10]*ms)
group = NeuronGroup(1, 'dv/dt = -v / (20*ms) : 1',
threshold='v > 1', reset='v = 0', method='exact')
syn = Synapses(inp, group, on_pre='v_post += 2')
syn.connect()
inp_mon = SpikeMonitor(inp)
group_mon = SpikeMonitor(group)
run(20*ms)
fig, ax = plt.subplots()
ax.plot(inp_mon.t/ms, [1], 'o')
ax.plot(group_mon.t/ms, [0], 'o')
ax.set(yticks=[0, 1], yticklabels=['group', 'inp'], xlabel='time (ms)');
As we can see, nothing changed… This should actually not come as much of a surprise: in the current schedule, the SpikeGeneratorGroup
will spike in the 10ms time step, but the activity gets only propagated to the target neuron in the time step that follows! We can also confirm this by looking at our trusted scheduling_summary
function:
scheduling_summary()
Before we continue, let us fix a minor ugliness in the output of scheduling_summary
. Brian uses names for each of its objects, but these names are determined automatically and can be hard to link back to the code. For example, which of the SpikeMonitor
s corresponds to spikemonitor
and spikemonitor_2
?
To make this link more obvious, we can give our own names to objects by specifying their name
argument. A useful choice is to simply chose the same name as for the variable in which we store the object:
start_scope()
inp = SpikeGeneratorGroup(1, [0], [10]*ms, name='inp')
group = NeuronGroup(1, 'dv/dt = -v / (20*ms) : 1',
threshold='v > 1', reset='v = 0', method='exact',
name='group')
syn = Synapses(inp, group, on_pre='v_post += 2', name='syn')
syn.connect()
inp_mon = SpikeMonitor(inp, name='inp_mon')
group_mon = SpikeMonitor(group, name='group_mon')
run(20*ms)
With the name
definitions, the scheduling_summary
output becomes more readable
scheduling_summary()
Now, back to our problem: how to we make the target neuron spike in the same time step as the SpikeGeneratorGroup
? We cannot simply change the global schedule, as this will move both the group_thresholder
(which determines when group
spikes) and the SpikeGeneratorGroup
around. Instead, we will have to move only one of the two operations around. We can do this by specifying the when
attribute, which determines into which slot an operation goes. For example, we can use before_synapses
(see the first notebook for Getting the timing right) slot for the SpikeGeneratorGroup
:
start_scope()
inp = SpikeGeneratorGroup(1, [0], [10]*ms, name='inp', when='before_synapses')
group = NeuronGroup(1, 'dv/dt = -v / (20*ms) : 1',
threshold='v > 1', reset='v = 0', method='exact',
name='group')
syn = Synapses(inp, group, on_pre='v_post += 2', name='syn')
syn.connect()
inp_mon = SpikeMonitor(inp, name='inp_mon')
group_mon = SpikeMonitor(group, name='group_mon')
run(20*ms)
run(20*ms)
fig, ax = plt.subplots()
ax.plot(inp_mon.t/ms, [1], 'o')
ax.plot(group_mon.t/ms, [0], 'o')
ax.set(yticks=[0, 1], yticklabels=['group', 'inp'], xlabel='time (ms)');
Finally, things work as intended, and we make our target neuron spike at 10ms! Of course, the change is reflected in the schedule:
scheduling_summary()
For more information on this topic, have a look at Brian’s documentation