NESTML language documentation¶
NESTML is a domain specific language for the specification of models for the neuronal simulator NEST. It has a concise syntax based on that of Python which avoids clutter in the form of semicolons, curly braces or tags as known from other programming and description languages. Instead, it concentrates on domain concepts that help to efficiently write neuron models and their equations.
NESTML files are expected to have the filename extension .nestml. Each file may contain one or more neuron models. This means that there is no forced direct correspondence between model and file name. Models that shall be compiled into one extension module for NEST have to reside in the same directory. The name of the directory will be used as the name of the corresponding module.
In order to give users complete freedom in implementing neuron model dynamics, NESTML has a full procedural programming language built in. This programming language is used to define the model’s time update and functions.
Data types and physical units¶
Data types define types of variables as well as parameters and return values of functions. NESTML provides the following primitive types and physical data types:
Primitive data types¶
realcorresponds to thedoubledata type in C++. Example literals are:42.0,-0.42,.44integercorresponds to thelongdata type in C++. Example literals are:42,-7booleancorresponds to thebooldata type in C++. Its only literals aretrueandfalsestringcorresponds to thestd::stringdata type in C++. Example literals are:"Bob","","Hello World!"voidcorresponds to thevoiddata type in C++. No literals are possible and this can only be used in the declaration of a function without a return value.
Physical units¶
A physical unit in NESTML can be either a simple physical unit or a complex physical unit. A simple physical unit is composed of an optional magnitude prefix and the name of the unit.
The following table lists seven base units, which can be used to specify any physical unit. This idea is based on the SI units.
Quantity |
Unit Name |
NESTML/SI unit |
|---|---|---|
length |
meter |
m |
mass |
kilogram |
kg |
time |
second |
s |
electric current |
ampere |
A |
temperature |
kelvin |
K |
amount of substance |
mole |
mol |
luminous intensity |
candela |
cd |
Any other physical unit can be expressed as a combination of these seven units. These other units are called derived units. NESTML provides a concept for the derivation of new physical units, i.e., by combining simple units (consisting of a prefix and an SI unit), the user is able to create arbitrary physical units.
Units can have at most one of the following magnitude prefixes:
Factor |
SI Name |
NESTML prefix |
Factor |
SI Name |
NESTML prefix |
|---|---|---|---|---|---|
10^-1 |
deci |
d |
10^1 |
deca |
da |
10^-2 |
centi |
c |
10^2 |
hecto |
h |
10^-3 |
milli |
m |
10^3 |
kilo |
k |
10^-6 |
micro |
mu |
10^6 |
mega |
M |
10^-9 |
nano |
n |
10^9 |
giga |
G |
10^-12 |
pico |
p |
10^12 |
tera |
T |
10^-15 |
femto |
f |
10^15 |
peta |
P |
10^-18 |
atto |
a |
10^18 |
exa |
E |
10^-21 |
zepto |
z |
10^21 |
zetta |
Z |
10^-24 |
yocto |
y |
10^24 |
yotta |
Y |
Simple physical units can be combined to complex units. For this, the operators , * (multiplication), / (division), ** (power) and () (parenthesis) can be used. An example could be
mV*mV*nS**2/(mS*pA)
Units of the form <unit> ** -1 can also be expressed as 1/<unit>. For example
(ms*mV)**-1
is equivalent to
1/(ms*mV)
NESTML also supports the usage of named derived-units such as Newton, Henry or lux:
Name |
Symbol |
Quantity |
In other SI units |
In base SI units |
|---|---|---|---|---|
radian |
rad |
angle |
m⋅m-1 |
|
steradian |
sr |
solid angle |
m2⋅m−2 |
|
Hertz |
Hz |
frequency |
s−1 |
|
Newton |
N |
force, weight |
kg⋅m⋅s−2 |
|
Pascal |
Pa |
pressure, stress |
N/m2 |
kg⋅m−1⋅s−2 |
Joule |
J |
energy, work, heat |
N⋅m=Pa⋅m3 |
kg⋅m2⋅s−2 |
Watt |
W |
power, radiant flux |
J/s |
kg⋅m2⋅s−3 |
Coulomb |
C |
electric charge or quantity of electricity |
s⋅A |
|
Volt |
V |
voltage (electrical potential), emf |
W/A |
kg⋅m2⋅s−3⋅ A−1 |
Farad |
F |
capacitance |
C/V |
kg−1⋅ m−2⋅ s4⋅ A2 |
Ohm |
Ω |
resistance, impedance, reactance |
V/A |
kg⋅(m2) ⋅ (s−3) ⋅(A−2) |
Siemens |
S |
electrical conductance |
Ω−1 |
(kg−1) ⋅(m−2) ⋅(s3) ⋅ A2 |
Weber |
Wb |
magnetic flux |
V⋅s |
kg⋅(m2) ⋅(s−2) ⋅(A−1) |
Tesla |
T |
magnetic flux density |
Wb/m2 |
kg⋅(s−2) ⋅(A−1) |
Henry |
H |
inductance |
Wb/A |
kg⋅(m2) ⋅(s−2) ⋅(A−2) |
lumen |
lm |
luminous flux |
cd⋅sr |
cd |
lux |
lx |
illuminance |
lm/m2 |
m−2⋅ cd |
Becquerel |
Bq |
radioactivity (decays per unit time) |
s−1 |
|
Gray |
Gy |
absorbed dose (of ionizing radiation) |
J/kg |
(m2)⋅(s−2) |
Sievert |
Sv |
equivalent dose (of ionizing radiation) |
J/kg |
(m2)⋅ (s−2) |
katal |
kat |
catalytic activity |
mol⋅(s−1) |
Here, except for Ohm, the symbol of the unit has to be used in the model, e.g.:
x = 10 N * 22 Ohm / 0.5 V
Physical unit literals¶
Simple unit literals are composed of a number and a type name (with or without a whitespace inbetween the two):
<number> <unit_type>
e.g.:
V_m mV = 1 mV
Complex unit literals can be composed according to the common arithmetic rules, i.e., by using operators to combine simple units:
V_rest = -55 mV/s**2
Type and unit checks¶
NESTML checks type correctness of all expressions. This also applies to assignments, declarations with an initialization and function calls. NESTML supports conversion of integers to reals. A conversion between unit-typed and real-typed variables is also possible. However, these conversions are reported as warnings. Finally, there is no conversion between numeric types and boolean or string types.
Basic elements of the embedded programming language¶
The basic elements of the language are declarations, assignments, function calls and return statements.
Declarations¶
Declarations are composed of a non-empty list of comma separated names. A valid name starts with a letter, an underscore or the dollar character. Furthermore, it can contain an arbitrary number of letters, numbers, underscores and dollar characters. Formally, a valid name satisfies the following regular expression:
( 'a'..'z' | 'A'..'Z' | '_' | '$' )( 'a'..'z' | 'A'..'Z' | '_' | '0'..'9' | '$' )*
Names of functions and input ports must also satisfy this pattern. The type of the declaration can be any of the valid NESTML types. The type of the initialization expression must be compatible with the type of the declaration.
<list_of_comma_separated_names> <type> (= initialization_expression)?
a, b, c real = -0.42
d integer = 1
n integer # default value is 0
e string = "foo"
f mV = -2e12 mV
It is legal to define a variable (or kernel, or parameter) with the same name as a physical unit, but this could lead to confusion. For example, defining a variable with name b creates an ambiguity with the physical unit b, a unit of surface area. In these cases, a warning is issued when the model is processed. The variable (or kernel, and parameter) definitions will then take precedence when resolving symbols: all occurrences of the symbol in the model will be resolved to the variable rather than the unit.
For example, the following model will result in one warning and one error:
neuron test:
state:
ms mA = 42 mA # redefine "ms" (from milliseconds unit to variable name)
foo s = 0 s # foo has units of time (seconds)
end
update:
ms = 1 mA # WARNING: Variable 'ms' has the same name as a physical unit!
foo = 42 ms # ERROR: Actual type different from expected. Expected: 's', got: 'mA'!
end
end
Documentation string¶
Each neuron model may be documented by a block of text in reStructuredText format. Following [PEP 257 “Docstring Conventions”](https://www.python.org/dev/peps/pep-0257/), this block should be enclosed in triple double quotes ("""…``”””``) and appear directly before the definition of the neuron. For example:
"""
iaf_psc_custom: My customized version of iaf_psc
################################################
Description
+++++++++++
Long description follows here. We can typeset LaTeX math:
.. math::
E = mc^2
"""
neuron iaf_psc_custom:
# [...]
end
This documentation block is rendered as HTML on the [NESTML Models Library](https://nestml.readthedocs.io/en/latest/models_library/index.html).
Comments in the model¶
When the character # appears as the first character on a line (ignoring whitespace), the remainder of that line is allowed to contain any comment string. Comments are not interpreted as part of the model specification, but when a comment is placed in a strategic location, it will be printed into the generated NEST code.
Example of single or multi-line comments:
var1 real # single line comment
# This is
# a comment
# over several lines.
To enable NESTML to recognize which element a comment belongs to, the following approach has to be used: there should be no white line separating the comment and its target. For example:
V_m mV = -55 mV # I am a comment of the membrane potential
# I am not a comment of the membrane potential. A white line separates us.
If a comment shall be attached to an element, no white lines are allowed.
V_m mV = -55 mV # I am a comment of the membrane potential
# I am a comment of the membrane potential.
Whitelines are therefore used to separate comment targets:
V_m mV = -55 mV
# I am a comment of the membrane potential.
# I am a comment of the resting potential.
V_rest mV = -60 mV
Assignments¶
NESTML supports simple or compound assignments. The left-hand side of the assignment is always a variable. The right-hand side can be an arbitrary expression of a type which is compatible with the left-hand side.
Examples for valid assignments for a numeric variable n are
simple assignment:
n = 10compound sum:
n += 10which corresponds ton = n + 10compound difference:
n -= 10which corresponds ton = n - 10compound product:
n *= 10which corresponds ton = n * 10compound quotient:
n /= 10which corresponds ton = n / 10
Functions¶
Functions can be used to write repeatedly used code blocks only once. They consist of the function name, the list of parameters and an optional return type, if the function returns a value to the caller. The function declaration ends with the keyword end.
function <name>(<list_of_arguments>) <return_type>?:
<statements>
end
e.g.:
function divide(a real, b real) real:
return a/b
end
To use a function, it has to be called. A function call is composed of the function name and the list of required parameters. The returned value (if any) can be directly assigned to a variable of the corresponding type.
<function_name>(<list_of_arguments>)
e.g.
x = max(a*2, b/2)
Predefined functions¶
The following functions are predefined in NESTML and can be used out of the box:
Name |
Parameters |
Description |
|---|---|---|
|
x, y |
Returns the minimum of x and y. Both parameters should be of the same type. The return type is equal to the type of the parameters. |
|
x, y |
Returns the maximum of x and y. Both parameters should be of the same type. The return type is equal to the type of the parameters. |
|
x, y, z |
Returns x if it is in [y, z], y if x < y and z if x > z. All parameter types should be the same and equal to the return type. |
|
x |
Returns the exponential of x. The type of x and the return type are Real. |
|
x |
Returns the base 10 logarithm of x. The type of x and the return type are Real. |
|
x |
Returns the base \(e\) logarithm of x. The type of x and the return type are Real. |
|
x |
Returns the exponential of x minus 1. The type of x and the return type are Real. |
|
x |
Returns the hyperbolic sine of x. The type of x and the return type are Real. |
|
x |
Returns the hyperbolic cosine of x. The type of x and the return type are Real. |
|
x |
Returns the hyperbolic tangent of x. The type of x and the return type are Real. |
|
mean, std |
Returns a sample from a normal (Gaussian) distribution with parameters “mean” and “standard deviation” |
|
offset, scale |
Returns a sample from a uniform distribution in the interval [offset, offset + scale) |
|
t |
A Dirac delta impulse function at time t. |
|
f, g |
The convolution of kernel f with spike input port g. |
|
s |
Log the string s with logging level “info”. |
|
s |
Log the string s with logging level “warning”. |
|
s |
Print the string s to stdout (no line break at the end). See print function for more information. |
|
s |
Print the string s to stdout (with a line break at the end). See print function for more information. |
|
This function can be used to integrate all stated differential equations of the equations block. |
|
|
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. |
|
|
t |
Convert a time into a number of simulation steps. See the section Handling of time for more information. |
|
Returns the current resolution of the simulation in ms. See the section Handling of time for more information. |
Return statement¶
The return keyword can only be used inside of the function block. Depending on the return type (if any), it is followed by an expression of that type.
return (<expression>)?
e.g.
if a > b:
return a
else:
return b
end
Printing output to the console¶
The print and println functions print a string to the standard output, with println printing a line break at the end. They can be used in the update block. See Block types for more information on the update block.
Example:
update:
print("Hello World")
...
println("Another statement")
end
Variables defined in the model can be printed by enclosing them in { and }. For example, variables V_m and V_thr used in the model can be printed as:
update:
...
print("A spike event with membrane voltage: {V_m}")
...
println("Membrane voltage {V_m} is less than the threshold {V_thr}")
end
end
Control structures¶
To control the flow of execution, NESTML supports loops and conditionals.
Loops¶
The start of the while loop is composed of the keyword while followed by a boolean condition and a colon. It is closed with the keyword end. It executes the statements inside the block as long as the given boolean expression evaluates to true.
while <boolean_expression>:
<statements>
end
e.g.:
x integer = 0
while x <= 10:
y = max(3, x)
end
The for loop starts with the keyword for followed by the name of a previously defined variable of type integer or real. The fist variant uses an integer stepper variable which iterates over the half-open interval [lower_bound, upper_bound) in steps of 1.
for <existing_variable_name> in <lower_bound> ... <upper_bound>:
<statements>
end
e.g.:
x integer = 0
for x in 1 ... 5:
# <statements>
end
The second variant uses an integer or real iterator variable and iterates over the half-open interval [lower_bound, upper_bound) with a positive integer or real step of size step. It is advisable to choose the type of the iterator variable and the step size to be the same.
for <existing_variable_name> in <lower_bound> ... <upper_bound> step <step>:
<statements>
end
e.g.:
x integer
for x in 1 ... 5 step 2:
# <statements>
end
x real
for x in 0.1 ... 0.5 step 0.1:
# <statements>
end
Conditionals¶
NESTML supports different variants of the if-else conditional. The first example shows the if conditional composed of a single if block:
if <boolean_expression>:
<statements>
end
e.g.:
if 2 < 3:
# <statements>
end
The second example shows an if-else block, which executes the if_statements in case the boolean expression evaluates to true and the else_statements else.
if <boolean_expression>:
<if_statements>
else:
<else_statements>
end
e.g.:
if 2 < 3:
# <if_statements>
else:
# <else_statements>
end
In order to allow grouping a sequence of related if conditions, NESTML also supports the elif-conditionals. An if condition can be followed by an arbitrary number of elif conditions. Optionally, this variant also supports the else keyword for a catch-all statement. The whole conditional always concludes with an end keyword.
if <boolean_expression>:
<if_statements>
elif <boolean_expression>:
<elif_statements>
else:
<else_statements>
end
e.g.:
if 2 < 3:
# <if_statements>
elif 4 > 6:
# <elif_statements>
else:
# <else_statements>
end
if 2 < 3:
# <if_statements>
elif 4>6:
# <elif_statements>
end
Conditionals can also be nested inside of each other.
if 1 < 4:
# <statements>
if 2 < 3:
# <statements>
end
end
Expressions and operators¶
Expressions in NESTML can be specified in a recursive fashion.
Terms¶
All variables, literals, and function calls are valid terms. Variables are names of user-defined or predefined variables (t, e).
List of operators¶
For any two valid numeric expressions a, b, boolean expressions c,c1,c2, and an integer expression n the following operators produce valid expressions.
Operator |
Description |
Examples |
|---|---|---|
|
Expressions with parentheses |
|
|
Power operator. |
|
|
unary plus, unary minus, bitwise negation |
|
|
Multiplication, Division and Modulo-Operator |
|
|
Addition and Subtraction |
|
|
Left and right bit shifts |
|
|
Bitwise |
|
|
Comparison operators |
|
|
Logical conjunction, disjunction and negation |
|
|
Ternary operator (return |
|
Blocks¶
To structure NESTML files, all content is structured in blocks. Blocks begin with a keyword specifying the type of the block followed by a colon. They are closed with the keyword end. Indentation inside a block is not mandatory but recommended for better readability. Each of the following blocks must only occur at most once on each level. Some of the blocks are required to occur in every neuron model. The general syntax looks like this:
<block_type> [<args>]:
...
end
Block types¶
neuron <name> - The top-level block of a neuron model called <name>. The content will be translated into a single neuron model that can be instantiated in PyNEST using nest.Create("<name>"). All following blocks are contained in this block.
Within the top-level block, the following blocks may be defined:
parameters- This block is composed of a list of variable declarations that are supposed to contain all parameters which remain constant during the simulation, but can vary among different simulations or instantiations of the same neuron. These variables can be set and read by the user usingnest.SetStatus(<gid>, <variable>, <value>)andnest.GetStatus(<gid>, <variable>).state- This block is composed of a list of variable declarations that describe parts of the neuron which may change over time. All the variables declared in this block must be initialized with a value.internals- This block is composed of a list of implementation-dependent helper variables that supposed to be constant during the simulation run. Therefore, their initialization expression can only reference parameters or other internal variables.equations- This block contains kernel definitions and differential equations. It will be explained in further detail later on in the manual.input- This block is composed of one or more input ports. It will be explained in further detail later on in the manual.output``<event_type>`` - Defines which type of event the neuron can send. Currently, onlyspikeis supported. Noendis necessary at the end of this block.update- Inside this block arbitrary code can be implemented using the internal programming language. Theupdateblock defines the runtime behavior of the neuron. It contains the logic for state and equation updates and refractoriness. This block is translated into theupdatemethod in NEST.
Neuronal interactions¶
Input¶
A neuron model written in NESTML can be configured to receive two distinct types of input: spikes and continuous-time values. For either of them, the modeler has to decide if inhibitory and excitatory inputs are lumped together into a single named input port, or if they should be separated into differently named input ports based on their sign. The input block is composed of one or more lines to express the exact combinations desired. Each line has the following general form:
port_name <- inhibitory? excitatory? (spike | continuous)
This way, a flexible combination of the inputs is possible. If, for example, current input should be lumped together, but spike input should be separated for inhibitory and excitatory incoming spikes, the following input block would be appropriate:
input:
I_stim pA <- continuous
inh_spikes pA <- inhibitory spike
exc_spikes pA <- excitatory spike
end
Please note that it is equivalent if either both inhibitory and excitatory are given or none of them at all. If only a single one of them is given, another line has to be present and specify the inverse keyword. Failure to do so will result in a translation error.
Integrating current input¶
The current port symbol (here, I_stim) is available as a variable and can be used in expressions, e.g.:
V_m' = -V_m/tau_m + ... + I_stim
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)
end
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
end
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, spikes:
inline h_dend pA = convolve(H, spikes)
The name 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
end
For more information, see the Active dendrite tutorial
Multiple input synapses¶
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
end
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)
function I_syn pA = convolve(I_kernel1, spikes1) - convolve(I_kernel2, spikes2) + convolve(I_kernel3, spikes3) + ...
V_abs' = -V_abs/tau_m + I_syn / C_m
end
After generating and building the model code, a receptor_type entry is available in the status dictionary, which maps port names to numeric port indices in NEST. The receptor type can then be selected in NEST during connection setup:
neuron = nest.Create("iaf_psc_exp_multisynapse_neuron_nestml")
sg = nest.Create("spike_generator", params={"spike_times": [20., 80.]})
nest.Connect(sg, neuron, syn_spec={"receptor_type" : 1, "weight": 1000.})
sg2 = nest.Create("spike_generator", params={"spike_times": [40., 60.]})
nest.Connect(sg2, neuron, syn_spec={"receptor_type" : 2, "weight": 1000.})
sg3 = nest.Create("spike_generator", params={"spike_times": [30., 70.]})
nest.Connect(sg3, neuron, syn_spec={"receptor_type" : 3, "weight": 500.})
Note that in multisynapse neurons, receptor ports are numbered starting from 1.
We furthermore wish to record the synaptic currents I_kernel1, I_kernel2 and I_kernel3. During code generation, one buffer is created for each combination of (kernel, spike input port) that appears in convolution statements. These buffers are named by joining together the name of the kernel with the name of the spike buffer using (by default) the string “__X__”. The variables to be recorded are thus named as follows:
mm = nest.Create('multimeter', params={'record_from': ['I_kernel1__X__spikes1',
'I_kernel2__X__spikes2',
'I_kernel3__X__spikes3'],
'interval': .1})
nest.Connect(mm, neuron)
The output shows the currents for each synapse (three bottom rows) and the net effect on the membrane potential (top row):
For a full example, please see tests/resources/iaf_psc_exp_multisynapse.nestml for the full model and tests/nest_tests/nest_multisynapse_test.py for the corresponding test harness that produced the figure above.
Output¶
Each neuron model can only send a single type of event. The type of the event has to be given in the output block. Currently, however, only spike output is supported.
output: spike
Please note that this block is not terminated with the end keyword.
Handling of time¶
To retrieve some fundamental simulation parameters, two special functions are built into NESTML:
resolutionreturns the current resolution of the simulation in ms. In NEST, this can be set by the user using the PyNEST functionnest.SetKernelStatus({"resolution": ...}).stepstakes one parameter of typemsand returns the number of simulation steps in the current simulation resolution.
These functions can be used to implement custom buffer lookup logic but should be used with care.
Equations¶
Systems of ODEs¶
In the equations block one can define a system of differential equations with an arbitrary amount of equations that contain derivatives of arbitrary order. When using a derivative of a variable, say V, one must write: V'. It is then assumed that V' is the first time derivate of V. The second time derivative of V is V'', and so on. If an equation contains a derivative of order \(n\), for example, \(V^{(n)}\), all initial values of \(V\) up to order \(n-1\) must be defined in the state block. For example, if stating
V' = a * V
in the equations block,
V mV = 0 mV
has to be defined in the state block. Otherwise, an error message is generated.
The content of spike and continuous time buffers can be used by just using their plain names. NESTML takes care behind the scenes that the buffer location at the current simulation time step is used.
Inline expressions¶
In the equations block, inline expressions may be used to reduce redundancy, or improve legibility in the model code. An inline expression is a named expression, that will be “inlined” (effectively, copied-and-pasted in) when its variable symbol is mentioned in subsequent ODE or kernel expressions. In the following example, the inline expression h_inf_T is defined, and then used in an ODE definition:
inline h_inf_T real = 1 / (1 + exp((V_m / mV + 83) / 4))
IT_h' = (h_inf_T * nS - IT_h) / tau_h_T / ms
Because of nested substitutions, inline statements may cause the expressions to grow to large size. In case this becomes a problem, it is recommended to use functions instead.
Kernel functions¶
A kernel is a function of time, or a differential equation, that represents a kernel which can be used in convolutions. For example, an exponentially decaying kernel could be described as a direct function of time, as follows:
kernel g = exp(-t / tau)
with time constant, for example, equal to 20 ms:
parameters:
tau ms = 20 ms
end
The start at time \(t \geq 0\) is an implicit assumption for all kernels.
Equivalently, the same exponentially decaying kernel can be formulated as a differential equation:
kernel g' = -g / tau
In this case, initial values have to be specified in the state block up to the order of the differential equation, e.g.:
state:
g real = 1
end
Here, the 1 defines the peak value of the kernel at \(t = 0\).
An example second-order kernel is the dual exponential (“alpha”) kernel, which can be defined in three equivalent ways.
As a direct function of time:
kernel g = (e/tau) * t * exp(-t/tau)
As a system of coupled first-order differential equations:
kernel g' = g$ - g / tau, g$' = -g$ / tau
with initial values:
state: g real = 0 g$ real = 1 end
Note that the types of both differential equations are \(\text{ms}^{-1}\).
As a second-order differential equation:
kernel g'' = (-2/tau) * g' - 1/tau**2) * g
with initial values:
state: g real = 0 g' ms**-1 = e / tau end
A Dirac delta impulse kernel can be defined by using the predefined function delta:
kernel g = delta(t)
Solver selection¶
Currently, there is support for GSL and exact integration. ODEs that can be solved analytically are integrated to machine precision from one timestep to the next. To allow more precise values for analytically solvable ODEs within a timestep, the same ODEs are evaluated numerically by the GSL solver. In this way, the long-term dynamics obeys the “exact” equations, while the short-term (within one timestep) dynamics is evaluated to the precision of the numerical integrator.
In the case that the model is solved with the GSL integrator, desired absolute error of an integration step can be adjusted with the gsl_error_tol parameter in a SetStatus call. The default value of gsl_error_tol is 1e-3.
Dynamics and time evolution¶
Inside the update block, the current time can be accessed via the variable t.
integrate_odes: this function can be used to integrate all stated differential equations of the equations block.
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.
Concepts for refractoriness¶
In order to model refractory and non-refractory states, two variables are necessary. The first variable (t_ref) defines the duration of the refractory period. The second variable (ref_counts) specifies the time of the refractory period that has already passed. It is initialized with 0 (the neuron is non-refractory) and set to the refractory offset every time the refractoriness condition holds. Else, the refractory offset is decremented.
parameters:
t_ref ms = 5 ms
end
internals:
ref_counts = 0
end
update:
if ref_count == 0: # neuron is in non-refractory state
if <refractoriness_condition>:
ref_counts = steps(t_ref) # make neuron refractory for 5 ms
end
else:
ref_counts -= 1 # neuron is refractory
end
end
Setting and retrieving model properties¶
All variables in the
stateandparametersblocks are added to the status dictionary of the neuron.Values can be set using
nest.SetStatus(<gid>, <variable>, <value>)where<variable>is the name of the corresponding NESTML variable.Values can be read using
nest.GetStatus(<gid>, <variable>). This call will return the value of the corresponding NESTML variable.
Recording values with devices¶
All values in the
stateblock are recordable by amultimeterin NEST.The
recordablekeyword can be used to also makeinlineexpressions in theequationsblock available to recording devices.
equations:
...
recordable inline V_m mV = V_abs + V_reset
end
Guards¶
Variables which are defined in the state and parameters blocks can optionally be secured through guards. These guards are checked during the call to nest.SetStatus() in NEST.
block:
<declaration> [[<boolean_expression>]]
end
e.g.:
parameters:
t_ref ms = 5 ms [[t_ref >= 0 ms]] # refractory period cannot be negative
end