Modeling neurons in NESTML
Writing the NESTML model
The top-level element of the model is
neuron, followed by a name. All other blocks appear inside of here.
neuron hodkin_huxley: # [...]
A neuron model written in NESTML can be configured to receive two distinct types of input: spikes and continuous-time values. This can be indicated using the following syntax:
input: I_stim pA <- continuous AMPA_spikes pA <- spike
The general syntax is:
port_name dataType <- inputQualifier* (spike | continuous)
For spiking input ports, the qualifier keywords decide whether inhibitory and excitatory inputs are lumped together into a single named input port, or if they are separated into differently named input ports based on their sign. When processing a spike event, some simulators (including NEST) use the sign of the amplitude (or weight) property in the spike event to indicate whether it should be considered an excitatory or inhibitory spike. By using the qualifier keywords, a single spike handler can route each incoming spike event to the correct input buffer (excitatory or inhibitory). Compare:
input: # [...] all_spikes pA <- spike
In this case, all spike events will be processed through the
all_spikes input port. A spike weight could be positive or negative, and the occurrences of
all_spikes in the model should be considered a signed quantity.
input: # [...] AMPA_spikes pA <- excitatory spike GABA_spikes pA <- inhibitory spike
In this case, spike events that have a negative weight are routed to the
GABA_spikes input port, and those that have a positive weight to the
It is equivalent if either both inhibitory and excitatory are given, or neither: an unmarked port will by default handle all incoming presynaptic spikes.
The incoming weight \(w\)…
… may be positive or negative. It is added to the buffer with signed value \(w\) (positive or negative).
… should not be negative. It is added to the buffer with non-negative magnitude \(w\).
… should be negative. It is added to the buffer with non-negative magnitude \(-w\).
Integrating current input
The current port symbol (here, I_stim) is available as a variable and can be used in expressions, e.g.:
equations V_m' = -V_m/tau_m + ... + I_stim input: I_stim pA <- continuous
Integrating spiking input
Spikes arriving at the input port of a neuron can be written as a spike train \(s(t)\):
To model the effect that an arriving spike has on the state of the neuron, a convolution with a kernel can be used. The kernel defines the postsynaptic response kernel, for example, an alpha (bi-exponential) function, decaying exponential, or a delta function. (See Kernel functions for how to define a kernel.) The convolution of the kernel with the spike train is defined as follows:
where \(w_i\) is the weight of spike \(i\).
For example, say there is a spiking input port defined named
spikes. A decaying exponential with time constant
tau_syn is defined as postsynaptic kernel
G. Their convolution is expressed using the
convolve(f, g) function, which takes a kernel and input port, respectively, as its arguments:
equations: kernel G = exp(-t/tau_syn) V_m' = -V_m/tau_m + convolve(G, spikes)
The type of the convolution is equal to the type of the second parameter, that is, of the spike buffer. Kernels themselves are always untyped.
(Re)setting synaptic integration state
When convolutions are used, additional state variables are required for each pair (shape, spike input port) that appears as the parameters in a convolution. These variables track the dynamical state of that kernel, for that input port. The number of variables created corresponds to the dimensionality of the kernel. For example, in the code block above, the one-dimensional kernel
G is used in a convolution with spiking input port
spikes. During code generation, a new state variable called
G__conv__spikes is created for this combination, by joining together the name of the kernel with the name of the spike buffer using (by default) the string “__conv__”. If the same kernel is used later in a convolution with another spiking input port, say
spikes_GABA, then the resulting generated variable would be called
G__conv__spikes_GABA, allowing independent synaptic integration between input ports but allowing the same kernel to be used more than once.
The process of generating extra state variables for keeping track of convolution state is normally hidden from the user. For some models, however, it might be required to set or reset the state of synaptic integration, which is stored in these internally generated variables. For example, we might want to set the synaptic current (and its rate of change) to 0 when firing a dendritic action potential. Although we would like to set the generated variable
G__conv__spikes to 0 in the running example, a variable by this name is only generated during code generation, and does not exist in the namespace of the NESTML model to begin with. To still allow referring to this state in the context of the model, it is recommended to use an inline expression, with only a convolution on the right-hand side.
For example, suppose we define:
inline g_dend pA = convolve(G, spikes)
Then the name
g_dend can be used as a target for assignment:
update: g_dend = 42 pA
This also works for higher-order kernels, e.g. for the second-order alpha kernel \(H(t)\):
kernel H'' = (-2/tau_syn) * H' - 1/tau_syn**2) * H
We can define an inline expression with the same port as before,
inline h_dend pA = convolve(H, spikes)
h_dend now acts as an alias for this particular convolution. We can now assign to the inline defined variable up to the order of the kernel:
update: h_dend = 42 pA h_dend' = 10 pA/ms
For more information, see the Active dendrite tutorial.
Multiple input ports
If there is more than one line specifying a spike or continuous port with the same sign, a neuron with multiple receptor types is created. For example, say that we define three spiking input ports as follows:
input: spikes1 nS <- spike spikes2 nS <- spike spikes3 nS <- spike
For the sake of keeping the example simple, we assign a decaying exponential-kernel postsynaptic response to each input port, each with a different time constant:
equations: kernel I_kernel1 = exp(-t / tau_syn1) kernel I_kernel2 = exp(-t / tau_syn2) kernel I_kernel3 = -exp(-t / tau_syn3) inline I_syn pA = convolve(I_kernel1, spikes1) - convolve(I_kernel2, spikes2) + convolve(I_kernel3, spikes3) V_m' = -(V_m - E_L) / tau_m + I_syn / C_m
Multiple input ports with vectors
The input ports can also be defined as vectors. For example, .. code-block:: nestml
- neuron multi_synapse_vectors:
AMPA_spikes pA <- excitatory spike GABA_spikes pA <- inhibitory spike NMDA_spikes pA <- spike foo pA <- spike exc_spikes pA <- excitatory spike inh_spikes pA <- inhibitory spike
kernel I_kernel_exc = exp(-1 / tau_syn_exc * t) kernel I_kernel_inh = exp(-1 / tau_syn_inh * t) inline I_syn_exc pA = convolve(I_kernel_exc, exc_spikes) inline I_syn_inh pA = convolve(I_kernel_inh, inh_spikes)
In this example, the spiking input ports
inh_spikes are defined as vectors. The integer surrounded by
] determines the size of the vector. The size of the input port must always be a positive-valued integer.
They could also be used in differential equations defined in the
equations block as shown for
inh_spikes in the example above.
emit_spike: calling this function in the
update block results in firing a spike to all target neurons and devices time stamped with the current simulation time.
Co-generation of neuron and synapse
update block in a NESTML model is translated into the
update method in NEST.