How to activate synaptic pathways on custom events only?

Description of problem

Hello, I have neurons with two types of events. I want to activate different synaptic pathways depending on 1) which event occurred and 2) whether it happened at the pre- or post-synaptic neuron. All the information I found in the documentation does not explain such a general case.

What you have already tried

import brian2 as br

n = br.NeuronGroup(2, model="dV/dt = cos(t/msecond)/msecond : 1",
                      threshold = "V>=0.3", reset="", events={'under':'V<0.3'})
n.V = [0, -0.5]

s = br.Synapses(n, n,
                model="""
                       a:1
                       b:1
                       c:1
                       d:1""",
                on_event={"path_a":"spike",
                          "path_b":"under",
                          "path_c":"spike",
                          "path_d":"under"},
                on_pre={"path_a":"a=a+1",
                        "path_b":"b=b+1"},
                on_post={"path_c":"c=c+1",
                         "path_d":"d=d+1"})
s.connect(i=0, j=1)
br.run(0.01*br.second)

Actual output

The spike and under events in this code are recognised correctly. However, both paths a and b get executed at presynaptic spikes, and paths c and d at postsynaptic spikes. Nothing seems to happen at the under event. I would like to have paths b and d happen and the pre- or postsynaptic under events. How can I do this?
Thank you

Hi @mmiekus, very interesting use case! This is a pity, our underlying machinery would support this effortlessly, but we are not exposing this in the Synapses interface. In fact, the on_event dictionary should only refer to pre and post, i.e. you can only set one event type for all pre and one event type for all post pathways. Fixing this in the code is almost trivial, but I’d need to have a more thorough look to make sure that this does not break anything. Do you necessarily need all these pathways to be in a single Synapses object, i.e. should they operate on the same variables? Otherwise, you could have two Synapses objects, each with a single pre/post event it listens to.

Thank you for the response. Unfortunately, I was planning to use the pathways to modify the same synaptic variables. However, the whole setup is actually my attempt at implementing a change in synaptic weight, which should happen when both pre- and postsynaptic neurons are above the threshold (without reset). And the change should happen only once, when the second of the neurons crosses the threshold (and not at each timestep). Maybe you would have any ideas if I can implement it somehow in a more straightforward way? I suppose I can always use some NetworkOperations.
A simpler question: is there an easy way to get an event only at the first timestep when the threshold condition is fulfilled?

Just a quick answer for this part: The usual way to do this is using a refractory condition, e.g. :

 n = br.NeuronGroup(..., threshold = "V>=0.3", refractory="V >= 0.3")

This means that the neuron fires when it crosses 0.3, but then is “refractory” and does no longer emit spikes until it falls under the threshold again. But then, this does not work for custom events, unfortunately… (see Refractoriness for custom events · Issue #868 · brian-team/brian2 · GitHub).

For custom events, the only workaround is currently to do the same thing but manually:

import brian2 as br
import matplotlib.pyplot as plt

n = br.NeuronGroup(1, """dV/dt = cos(t/msecond)/msecond : 1
                         is_under : boolean""",
                    events={'under': 'V<0.3 and not is_under',
                            'over': 'V>=0.3 and is_under'})
# initial value needs to be correct:
n.is_under = True
n.run_on_event('under', 'is_under = True')
n.run_on_event('over', 'is_under = False')
state_mon = br.StateMonitor(n, 'V', record=True)
under_mon = br.EventMonitor(n, 'under')
over_moon = br.EventMonitor(n, 'over')
br.run(10*br.ms)

plt.plot(state_mon.t/br.ms, state_mon.V[0])
plt.plot(under_mon.t/br.ms, [0.3]*len(under_mon.t), 'v', color='C1')
plt.plot(over_moon.t/br.ms, [0.3]*len(over_moon.t), '^', color='C2')
plt.show()

This makes sure that the under and over events only fire whenever they go over/under 0.3 for the first time:
Figure_1

Thank you very much!

Hah, if I understand correctly, this should actually be quite straightforward to implement, and doesn’t even need custom events :blush: The trick is to only care about the spike event of the second neuron, but make your weight update conditional on the membrane potential of the first neuron by multiplying it by int(V_pre >= 0.3). This expression will be 0 or 1 depending on whether the pre-synaptic neuron is above the threshold.

Here’s an example:

import brian2 as br
import matplotlib.pyplot as plt
n = br.NeuronGroup(2, model="""dV/dt = cos(2*pi*freq*t)/msecond : 1
                               freq: Hz (constant)""",
                      threshold = "V>=0.3", reset="", refractory="V>=0.3")
n.V = [0, -0.5]
n.freq = [100, 150]*br.Hz
weight_update = 0.1
s = br.Synapses(n, n,
                model="w : 1",
                # update on post-synaptic spike, but only if pre-synaptic is above threshold as well
                on_post="w += int(V_pre>=0.3)*weight_update")
s.connect(i=0, j=1)
state_mon = br.StateMonitor(n, 'V', record=True)
weight_mon = br.StateMonitor(s, 'w', record=True)
spike_mon = br.SpikeMonitor(n)
br.run(0.05*br.second)
plt.plot(state_mon.t/br.ms, state_mon.V[0], color='C0')
plt.plot(state_mon.t/br.ms, state_mon.V[1], color='C1')
post_spikes = spike_mon.spike_trains()[1]
plt.plot(post_spikes/br.ms, [0.3]*len(post_spikes), '^', color='C1')
ax_2 = plt.twinx()
ax_2.plot(weight_mon.t/br.ms, weight_mon.w.T, 'C2')
plt.show()

Figure_1
As you can see, the synaptic weight (green line), is only updated when the post-synaptic neuron (orange line) fires and the pre-synaptic neuron (blue line) is above threshold at the same time.

Hope that gets you going!

1 Like

Yes! Thank you! This is more or less what I needed.

1 Like