Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding attributes to spike events #946

Open
4 of 7 tasks
clinssen opened this issue Aug 23, 2023 · 2 comments · May be fixed by #1137
Open
4 of 7 tasks

Adding attributes to spike events #946

clinssen opened this issue Aug 23, 2023 · 2 comments · May be fixed by #1137

Comments

@clinssen
Copy link
Contributor

clinssen commented Aug 23, 2023

Currently, neurons can emit spikes by calling the NESTML predefined function emit_spike(), without any arguments. Synapses can emit spikes by calling emit_spike(w, d), passing the weight and delay parameters.

It would be great if we could unify these functions and make them more generic. Could we rewrite this in a way where there is a generic "event", to which we can attach attributes (like weight and delay, or multiplicity, or any named parameter at all)? Then we could do something like:

emit_spike()    # neuron emits unweighted spikes, no parameters needed
emit_spike(w_, 1 ms)    # synapse emits a spike with a first attribute which gets its value 
                        # from the local variable w_, and a second attribute which is assigned a
                        # constant value here

These attributes could subsequently be read out in an event handler as follows:

onReceive(exc_spikes):
    I_syn_exc += exc_spikes.weight
    foo += exc_spikes.multiplicity

as opposed to the current implementation, where exc_spikes is defined as a sum of delta pulses (with physical units 1/s):

onReceive(exc_spikes):
    I_syn_exc += exc_spikes * pA * s

In the new implementation, the event attribute "weight" would simply have the units of whatever we put in on the sending side, but we would need a way to explicitly specify the expected unit on the receiving side, which might look something like this:

input:
    exc_spikes <- spike(weight real, delay ms)

Possibly, the attributes and types should also be specified as part of the output port:

output:
    spike(weight real, delay ms)

One issue is that naming conventions might be different between the sending side and receiving side (for instance, "w" and "weight"). We could use the code generator options dictionary to make a mapping between these, as we are already doing for the port names currently. Each attribute that is needed on the receiving side should be mapped to attributes of the sender's spike event, but it should be allowed for a sender to attach all kinds of attributes to its output events that will be potentially unused.

In case there are no event attributes, the port name cannot be accessed inside the onReceive block, for instance:

input:
    exc_spikes <- spike

onReceive(exc_spikes):
    foo += exc_spikes    # error!

Vector input ports

Possibly related is the support for a vector of input ports. Constructs like onReceive(exc_spikes[5]) should be allowed if exc_spikes is defined as a vector of suitable length.

Consider the follow example, but where exc_spikes is a vector:

onReceive(exc_spikes):
    I_syn_exc += exc_spikes.weight

Is it implied that exc_spikes.weight is also a vector?

Named parameters

Instead of

emit_spike(w_, 1 ms)    # synapse emits a spike with a first attribute which gets its value 
                        # from the local variable w_, and a second attribute which is assigned a
                        # constant value here

we could name parameters in the style of Python:

emit_spike(weight=w_, delay=1 ms)    # synapse emits a spike with an attribute "weight" which gets its value 
                        # from the local variable w_, and an attribute "delay" which is assigned a
                        # constant value here

For this feature, see #1122.

Types of convolutions

In case of a spiking input port without any attributes:

input:
    spike_in_port <- spike

It should be allowed to write a convolution with that input port. For example:

x' = -x / tau + convolve(K, spike_in_port)

Because $K$ can be a $\delta$-function, it should also be possible to write:

x' = -x / tau + spike_in_port

or, in a variant that includes attributes:

x' = -x / tau + spike_in_port.weight

Mathematical definition of input port

If there are no attributes, the spike input port variable spike_in_port is interpreted as a series of delta pulses:

$$ spike\_in\_port(t) = \sum_k \delta(t - t_k) $$

This has the physical unit [1/s].

If there are attributes, they are similarly defined; for instance, for the weight

$$ spike\_in\_port.weight(t) = \sum_k weight_k \cdot \delta(t - t_k) $$

If the weight is in pA, this has the physical unit [pA/s].

Example for a convolution:

$$ \frac{dV}{dt} = -\frac{V}{\tau} + \frac 1 C \left(K \ast spike\_in\_port.weight\right) $$

$spike\_in\_port.weight$ has the unit [pA/s] in this example. Then the unit of the convolution is [pA], which is multiplied by [1/Farad] = [1/(A*s/V)] to yield [V/s], consistent with the left-hand side of the ODE.

Example for an event handler

onReceive(spike_in_port):
    I_syn += spike_in_port.weight

Here, it does it make sense to think of spike_in_port.weight as a function of time? We are evaluating it precisely at the instant of the event; say that in the onReceive block, the trains of delta pulses are implictly convolved with a delta kernel, which takes out the unit [1/s]:

$$ w_k(t_k) = \left(\delta(t) \ast spike\_in\_port.weight(t)\right)(t_k) $$

Then, on the left and right-hand side, we have something in units of [pA]. In pseudo-NESTML code:

For each spike at t_k with weight spike_in_port.weight(t_k):
    onReceive(spike_in_port):                   # spike at t_k
        I_syn += spike_in_port.weight           # weight w_k = conv(delta(t), spike_in_port.weight(t))(t_k)

However, what about the original example, where we could write:

onReceive(spike_in_port):
    V += spike_in_port.weight / C

In this case, we would have [V] on the left-hand side and [pA/s] / [A*s/V] = [V / s**2] on the right, which is clearly inconsistent. But perhaps the constant of proportionality ($C$) here is the wrong choice, and something in terms of charge should be formulated, as we are dealing with an instantaneous event.

Alternative

A predefined attribute spike_in_port.times could be defined, that would be the sum of delta pulses and have units [1/s], while the other attributes (such as spike_in_port.weights) would be defined as [pA].

Necessary documentation

  • Make it clear that each spike from each distinct presynaptic neuron is handled by an individual run of the event handler (the events in the onReceive block).

Necessary cocos

  • continuous-type input ports do not support attributes
  • continuous-type output ports do not support attributes
  • for each emit_spike() function call, check that types correspond to the ones defined in the output block
  • for each occurrence of name.attribute, if name is resolved to an input port name, check that attribute is amongst the attributes defined in the input port declaration
  • (?) disallow spike attribute names to alias variable names in the model scope (such as in the state or parameters block), e.g. "spike_port.foo" is not allowed when a state variable or parameter named "foo" is defined
  • Spiking input ports may appear only onthe right-hand side of equations and inline expressions, and inside event handlers (referring the the value of a spiking input port makes no sense in the update block: would we be getting the value at the start of the block (t), at the end of the block (t+Δt) or somewhere inbetween?
@pnbabu
Copy link
Contributor

pnbabu commented Sep 25, 2024

As discussed in the NESTML meeting, the syntax for the onReceive block with vector input ports will be as follows:

input:
  spikes[3] <- spike(weight:real, multiplicity: real)

onReceive(spikes[0]):
  I_syn += spikes[0]

onReceive(spikes[1]):
  # using weight and multiplicity of the spike event
  I_syn += spikes[1].weight * spikes[1].multiplicity

onReceive(spikes[2]):
  ...

Each input port of the vector spikes[3] would have to be individually handled in their own onReceive block. Each of these spiking events will have its attributes, for example, weight, multiplicity, delay, etc, which can be specified as spikes[1].weight.

The input port cannot be used on its own without a vector index in an onReceive block. Thus, the following syntax is not allowed:

onReceive(spikes)
  I_syn += spikes[0] + spikes[1] + ...

@tomtetzlaff
Copy link
Collaborator

tomtetzlaff commented Nov 25, 2024

(comment on Mathematical definition of input port above)

I find it problematic to use the same quantity spike_in_port.weight in

x' = -x / tau + spike_in_port.weight

and in

onReceive(spike_in_port):
    x += spike_in_port.weight

Mathematically, this is wrong. The units on the lhs and on the rhs must be identical. The idea that the spike_in_port.weight changes its meaning depending on whether it is used in an ODE (or convolution) or within an onReceive block is very confusing. I'd find it much clearer and more intuitive if spike_in_port.weight corresponded to the actual synaptic weight, i.e., the amplitude of an incoming spike (in units of pA, mV, nS, etc), and if the weighted delta functions were represented by something else, say spike_in_port.weighted_spikes. Like this:

spike_in_port.weighted_spikes$=\sum_{j}\sum_k J_j \delta(t-t_j^k-d_j)$

spike_in_port.weight$=J_j$

spike_in_port.delay $=d_j$

With this, we could write:

x' = -x / tau + spike_in_port.weighted_spikes

and

onReceive(spike_in_port):
    x += spike_in_port.weight

(to make it shorter, one could perhaps replace spike_in_port.weighted_spikes by just spike_in_port).

As far as I understand, the construct onReceive(spike_in_port) represents an individual spike event

a) at a given point in time

b) from a single presynaptic neuron.

If multiple spikes from different presynaptic neurons arrive at the same time, each of these spikes "invokes" its own private onReceive block. Hence, the quantity spike_in_port.weight has no meaning outside the onReceive block, because its actual value depends on the specific presynaptic neuron, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants