Parser
A Neuron or Synapse type is primarily defined by two sets of values which must be specified in its constructor:
- Parameters are values such as time constants which are constant during the simulation. They can be the same throughout the population/projection, or take different values.
- Variables are neuronal variables (for example the membrane potential or firing rate) or synaptic variables (the synaptic efficiency) whose value evolve with time during the simulation. The equation (whether it is an ordinary differential equation or not) ruling their evolution can be described using a specific meta-language.
Parameters
Parameters are defined by a multi-string consisting of one or more parameter definitions:
= """
parameters tau = 10.0
eta = 0.5
"""
Each parameter should be defined on a single line, with its name on the left side of the equal sign, and its value on the right side. The given value corresponds to the initial value of the parameter (but it can be changed at any further point of the simulation).
As a neuron/synapse type is likely to be reused in different populations/projections, it is good practice to set reasonable initial values in the neuron/synapse type, and eventually adapt them to the corresponding populations/projections later on.
Local vs. global parameters
By default, a neural parameter will be unique to each neuron (i.e. each neuron instance will hold a copy of the parameter) or synapse. In order to save memory space, one can force ANNarchy to store only one parameter value for a whole population by specifying the population
flag after a :
symbol following the parameter definition:
= """
parameters tau = 10.0
eta = 0.5 : population
"""
In this case, there will be only only one instance of the eta
parameter for the whole population. eta
is called a global parameter, in opposition to local parameters which are the default.
The same is true for synapses, whose parameters are by default unique to each synapse in a given projection. If the post-synaptic
flag is passed, the parameter will be common to all synapses of a post-synaptic neuron, but can differ from one post-synaptic neuron to another. If the projection
flag is passed, the parameter will be common to all synapses of a projection (e.g. the learning rate).
Type of the variable
Parameters have floating-point precision by default. If you want to force the parameter to be an integer or boolean, you can also pass the int
and bool
flags, separated by commas:
= """
parameters tau = 10.0
eta = 1 : population, int
"""
Constants
Alternatively, it is possible to use constants in the parameter definition (see later):
= ann.Constant('tau_exc', 10.0)
tau_exc
= ann.Neuron(
neuron = """
parameters tau = tau_exc
""",
)
The advantage of this method is that if a parameter value is "shared" across several neuron/synapse types, you only need to change the value once, instead of in each neuron/synapse definition.
Variables
Time-varying variables are also defined using a multi-line description:
= """
equations noise = Uniform(0.0, 0.2)
tau * dv/dt + v = baseline + sum(exc) + noise
r = pos(v)
"""
The evolution of each variable with time can be described through a simple equation or an ordinary differential equation (ODE). ANNarchy provides a simple parser for mathematical expressions, whose role is to translate a high-level description of the equation into an optimized C++ code snippet.
The equation for one variable can depend on parameters, other variables (even when declared later) or constants. Variables are updated in the same order as their declaration in the multistring (see Numerical methods, as it influences how ODEs are solved).
The declaration of a single variable can extend on multiple lines:
= """
equations noise = Uniform(0.0, 0.2)
tau * dv/dt = baseline - v
+ sum(exc) + noise : max = 1.0
r = pos(v)
"""
As it is only a parser and not a solver, some limitations exist:
- Simple equations must hold only the name of the variable on the left sign of the equation. Variable definitions such as
r + v = noise
are forbidden, as it would be impossible to guess which variable should be updated. - ODEs are more free regarding the left side, but only one variable should hold the gradient: the one which will be updated. The following definitions are equivalent and will lead to the same C++ code:
tau * dv/dt = baseline - v
tau * dv/dt + v = baseline
tau * dv/dt + v - baseline = 0
dv/dt = (baseline - v) / tau
In practice, ODEs are transformed using Sympy into the last form (only the gradient stays on the left) and numerized using the chosen numerical method (see Numerical methods).
Flags
Locality and type
Like the parameters, variables also accept the population
, postsynaptic
and projection
flags to define the local/global character of the variable, as well as the int
or bool
flags for their type.
Initial value
The initial value of the variable (before the first simulation starts) can also be specified using the init
keyword followed by the desired value:
= """
equations tau * dv/dt + v = baseline : init = 0.2
"""
It must be a single value (the same for all neurons in the population or all synapses in the projection) and should not depend on other parameters and variables. This initial value can be specifically changed after the Population
or Projection
objects are created (see Populations).
It is also possible to use constants for the initial value:
= Constant('init_v', 0.2)
init_v
= Neuron(
neuron = """
equations tau * dv/dt + v = baseline : init = init_v
""",
)
Min and Max values of a variable
Upper- and lower-bounds can be set using the min
and max
keywords:
= """
equations tau * dv/dt + v = baseline : min = -0.2, max = 1.0
"""
At each step of the simulation, after the update rule is calculated for v
, the new value will be compared to the min
and max
value, and clamped if necessary.
min
and max
can be single values, constants, parameters, variables or functions of all these:
= """
parameters tau = 10.0
min_v = -1.0 : population
max_v = 1.0
""",
= """
equations variance = Uniform(0.0, 1.0)
tau * dv/dt + v = sum(exc) : min = min_v, max = max_v + variance
r = v : min = 0.0 # Equivalent to r = pos(mp)
"""
Numerical method
The numerization method for a single ODEs can be explicitely set by specifying a flag:
tau * dv/dt + v = sum(exc) : exponential
The available numerical methods are described in Numerical methods.
Summary of allowed flags for variables:
init
: defines the initialization value at begin of simulation and after a network reset (default: 0.0)min
: minimum allowed value (unset by default)max
: maximum allowed value (unset by default)population
: the attribute is shared by all neurons of a population.postsynaptic
: the attribute is shared by all synapses of a post-synaptic neuron.projection
: the attribute is shared by all synapses of a projection.explicit
,implicit
,exponential
,midpoint
,event-driven
: the numerical method to be used.
Constants
Global constants can be created by the user and used inside any equation. They must define an unique name and a floating point value:
= ann.Constant('tau', 10.0)
tau
= ann.Neuron(
neuron = "tau * dr/dt + r = sum(exc)"
equations )
In this example, a Neuron or Synapse does not have to define the parameter tau
to use it: it is available everywhere. If the Neuron/Synapse redefines a parameter called tau
, the constant is not visible anymore to that object.
Constants can be manipulated as normal floats to define complex values:
= ann.Constant('tau', 20)
tau = ann.Constant('factor', 0.1)
factor = ann.Constant('real_tau', tau*factor)
real_tau
= ann.Neuron(
neuron ='''
equations real_tau*dr/dt + r =1.0
'''
)
Note that constants are only global, changing their value impacts all objects using them. Changing the value of a constant can only be done through the set()
method (before or after compile()
):
= ann.Constant('tau', 20)
tau set(10.0) tau.
Allowed vocabulary
The mathematical parser relies heavily on the one provided by SymPy.
Numerical values
All parameters and variables use implicitly the floating-point double precision, except when stated otherwise with the int
or bool
keywords. You can use numerical constants within the equation, noting that they will be automatically converted to this precision:
tau * dv / dt = 1 / pos(v) + 1
The constant \pi is available under the literal form pi
.
Operators
- Additions (+), substractions (-), multiplications (*), divisions (/) and power functions (^) are of course allowed.
- Gradients are allowed only for the variable currently described. They take the form:
dv / dt = A
with a d
preceding the variable’s name and terminated by /dt
(with or without spaces). Gradients must be on the left side of the equation.
- To update the value of a variable at each time step, the operators
=
,+=
,-=
,*=
, and/=
are allowed.
Parameters and Variables
Any parameter or variable defined in the same Neuron/Synapse can be used inside an equation. User-defined constants can also be used. Additionally, the following variables are pre-defined:
dt
: the discretization time step for the simulation. Using this variable, you can define the numerical method by yourself. For example:
* dv / dt + v = baseline tau
with backward Euler would be equivalent to:
+= dt/tau * (baseline - v) v
t
: the time in milliseconds elapsed since the creation of the network. This allows to generate oscillating variables:
= 10.0 # Frequency of 10 Hz
f = pi/4 # Phase
phi = t / 1000.0 # ts is in seconds
ts = 10.0 * (sin(2*pi*f*ts + phi) + 1.0) r
Random number generators
Several random generators are available and can be used within an equation, for example:
Uniform(min, max)
generates random numbers from a uniform distribution in the range [\text{min}, \text{max}].Normal(mu, sigma)
generates random numbers from a normal distribution with mean mu and standard deviation sigma.
See the reference for Random Distributions for more distributions. For example:
noise = Uniform(-0.5, 0.5)
The arguments to the random distributions can be either fixed values or (functions of) global parameters.
min_val = -0.5 : population
max_val = 0.5 : population
noise = Uniform(min_val, max_val)
It is not allowed to use local parameters (with different values per neuron) or variables, as the random number generators are initialized only once at network creation (doing otherwise would impair performance too much). If a global parameter is used, changing its value will not affect the generator after compilation.
It is therefore better practice to use normalized random generators and scale their outputs:
= -0.5 : population
min_val = 0.5 : population
max_val = min_val + (max_val - min_val) * Uniform(0.0, 1.0) noise
Mathematical functions
- Most mathematical functions of the
cmath
library are understood by the parser, for example:
abs, fabs, sqrt, log, ln cos, sin, tan, acos, asin, atan, exp,
- The positive and negative parts of a term are also defined, with short and long versions:
= pos(mp)
r = positive(mp)
r = neg(mp)
r = negative(mp) r
- A piecewise linear function is also provided (linear when x is between a and b, saturated at a or b otherwise):
= clip(x, a, b) r
- For integer variables, the modulo operator is defined:
+= 1 : int
x = modulo(x, 10) y
- When using the power function (
r = x^2
orr = pow(x, 2)
), thecmath
pow(double, int)
method is used. For small exponents (quadratic or cubic functions), it can be extremely slow, compared tor = x*x
orr = x*x*x
. Unfortunately, Sympy transforms automaticallyr = x*x
intor = pow(x, 2)
. We therefore advise to use the built-inpower(double, int)
function instead:
= power(x, 3) r
These functions must be followed by a set of matching brackets:
* dmp / dt + mp = exp( - cos(2*pi*f*t + pi/4 ) + 1) tau
Conditional statements
Python-style
It is possible to use Python-style conditional statements as the right term of an equation or ODE. They follow the form:
if condition : statement1 else : statement2
For example, to define a piecewise linear function, you can nest different conditionals:
= if mp < 1. :
r if mp > 0.:
mpelse:
0.
else:
1.
which is equivalent to:
= clip(mp, 0.0, 1.0) r
The condition can use the following vocabulary:
True, False, and, or, not, is, is not, ==, !=, >, <, >=, <=
The and
, or
and not
logical operators must be used with parentheses around their terms. Example:
= if (mp > 0) and ( (noise < 0.1) or (not(condition)) ):
var 1.0
else:
0.0
is
is equivalent to ==
, is not
is equivalent to !=
.
When a conditional statement is split over multiple lines, the flags must be set after the last line:
= if mp < 1.0 :
rate if mp < 0.0 :
0.0
else:
mpelse:
1.0 : init = 0.6
An if a: b else:c
statement must be exactly the right term of an equation. It is for example NOT possible to write:
r = 1.0 + (if mp> 0.0: mp else: 0.0) + b
Ternary operator
The ternary operator ite(cond, then, else)
(ite stands for if-then-else) is available to ease the combination of conditionals with other terms:
= ite(mp>0.0, mp, 0.0)
r # is exactly the same as:
= if mp > 0.0: mp else: 0.0 r
The advantage is that the conditional term is not restricted to the right term of the equation, and can be used multiple times:
= ite(mp > 0.0, ite(mp < 1.0, mp, 1.0), 0.0) + ite(stimulated, 1.0, 0.0) r
Custom functions
To simplify the writing of equations, custom functions can be defined either globally (usable by all neurons and synapses) or locally (only for the particular type of neuron/synapse) using the same mathematical parser.
Global functions can be defined using the add_function()
method:
'sigmoid(x) = 1.0 / (1.0 + exp(-x))') add_function(
With this declaration, sigmoid()
can be used in the declaration of any variable, for example:
= ann.Neuron(
neuron = """
equations r = sigmoid(sum(exc))
"""
)
Functions must be one-liners, i.e. they should have only one return value. They can use as many arguments as needed, but are totally unaware of the context: all the needed information should be passed as an argument (except constants which are visible to the function).
The types of the arguments (including the return value) are by default floating-point. If other types should be used, they should be specified at the end of the definition, after the :
sign, with the type of the return value first, followed by the type of all arguments separated by commas:
'conditional_increment(c, v, t) = if v > t : c + 1 else: c : int, int, float, float') ann.add_function(
After compilation, the function can be called using arbitrary list of values for the arguments using the functions()
method and the name of the function:
'sigmoid(x) = 1.0 / (1.0 + exp(-x))')
ann.add_function(
compile()
= np.linspace(-10., 10., 1000)
x = ann.functions('sigmoid')(x) y
You can pass a list or a 1D Numpy array as argument, but not a single value or a multidimensional array. When several arguemnts are passed, they must have the same size.
Local functions are specific to a Neuron or Synapse class and can only be used within this context (if they have the same name as global variables, they will override them). They can be passed as a multi-line argument to the constructor of a neuron or synapse (see later):
== """
functions sigmoid(x) = 1.0 / (1.0 + exp(-x))
conditional_increment(c, v, t) = if v > t : c + 1 else: c : int, int, float, float
"""