1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
|
==================
Parameter handling
==================
.. testsetup::
import numpy
from lazyarray import larray
.. module:: pyNN.parameters
.. note:: these classes are not part of the PyNN API. They should not be used in
PyNN scripts, they are intended for implementing backends. You are not
required to use them when implementing your own backend, however, as
long as your backend conforms to the API.
The main abstractions in PyNN are the population of neurons, and the set of
connections (a 'projection') between two populations. Setting the parameters of
individual neuron and synapse models, therefore, mainly takes place at the level
of populations and projections.
.. note:: it is also possible to set the parameters of neurons and synapses
individually, but this is generally less efficient.
Any model parameter in PyNN can be expressed as:
* a single value - all neurons in a population or synapses in a projection get
the same value
* a :class:`RandomDistribution` object - each element gets a value drawn from
the random distribution
* a list/array of values of the same size as the population/projection
* a mapping function, where a mapping function accepts a either a single
argument ``i`` (for a population) or two arguments ``(i, j)`` (for a
projection) and returns a single value.
A "single value" is usually a single number, but for some parameters (e.g. for
spike times) it may be a list/array of numbers.
To handle all these possibilities in a uniform way, and at the same time allow
for efficient parallelization, in the 'common' implementation of the PyNN API
all parameter values are converted into :class:`LazyArray` objects, and the
set of parameters for a model is contained in a :class:`dict`-like object,
:class:`ParameterSpace`.
The :class:`LazyArray` class
----------------------------
:class:`LazyArray` is a PyNN-specific sub-class of a more general class,
:class:`larray`, and most of its functionality comes from the parent class. Full
documentation for :class:`larray` is available in the lazyarray_ package, but we
give here a quick overview.
:class:`LazyArray` has three important features in the context of PyNN:
1. any operations on the array (potentially including array construction) are
not performed immediately, but are delayed until evaluation is specifically
requested.
2. evaluation of only parts of the array is possible, which means that in a
parallel simulation with MPI, all processes have the same :class:`LazyArray`
for a parameter, but on a given process, only the part of the array which
is needed for the neurons/synapses that exist on that process need be
evaluated.
3. single often all neurons in a population or synapses in a projection have
the same value for a given parameter, a :class:`LazyArray` created from a
single value evaluates to that value: the full array is never created
unless this is requested.
For example, suppose we have two parameters, `tau_m`, which is constant,
and `v_thresh` which varies according to the position of the neuron in the
population.
.. doctest::
>>> from pyNN.parameters import LazyArray
>>> tau_m = 2 * LazyArray(10.0, shape=(20,))
>>> v_thresh = -55 + LazyArray(lambda i: 0.1*i, shape=(20,))
If we evaluate `tau_m` we get a full, homogeneous array:
.. doctest::
>>> tau_m.evaluate()
array([ 20., 20., 20., 20., 20., 20., 20., 20., 20., 20., 20.,
20., 20., 20., 20., 20., 20., 20., 20., 20.])
but we could also have asked just for the single number, in which case the
full array would never be created:
.. doctest::
>>> tau_m.evaluate(simplify=True)
20.0
Similarly, we can evaluate `v_thresh` to get a normal NumPy array:
.. doctest::
>>> v_thresh.evaluate()
array([-55. , -54.9, -54.8, -54.7, -54.6, -54.5, -54.4, -54.3, -54.2,
-54.1, -54. , -53.9, -53.8, -53.7, -53.6, -53.5, -53.4, -53.3,
-53.2, -53.1])
but we can also take, for example, only every fifth value, in which case the
operation "add -55" only gets performed for those elements.
.. doctest::
>>> v_thresh[::5]
array([-55. , -54.5, -54. , -53.5])
In this example the operation is very fast, but with slower operations (e.g.
distance calculations) and large arrays, the time savings can be considerable
(see `lazyarray performance`_).
In summary, by using :class:`LazyArray`, we can pass parameters around in an
optimised way without having to worry about exactly what form the parameter
value takes, hence avoiding a lot of logic at multiple points in the code.
Reference
~~~~~~~~~
.. autoclass:: pyNN.parameters.LazyArray
:members:
:undoc-members:
:inherited-members:
:show-inheritance:
.. automethod:: pyNN.parameters.LazyArray.__getitem__
The :class:`ParameterSpace` class
---------------------------------
:class:`ParameterSpace` is a dict-like class that contains :class:`LazyArray`
objects.
In addition to the usual :class:`dict` methods, it has several methods
that allow operations on all the lazy arrays within it at once. For example:
.. doctest::
>>> from pyNN.parameters import ParameterSpace
>>> ps = ParameterSpace({'a': [2, 3, 5, 8], 'b': 7, 'c': lambda i: 3*i+2}, shape=(4,))
>>> ps['c']
<larray: base_value=<function <lambda> at ...> shape=(4,) dtype=None, operations=[]> # doctest: +ELLIPSIS
>>> ps.evaluate()
>>> ps['c']
array([ 2, 5, 8, 11])
the :meth:`evaluate()` method also accepts a mask, in order to evaluate only
part of the lazy arrays:
.. doctest::
>>> ps = ParameterSpace({'a': [2, 3, 5, 8, 13], 'b': 7, 'c': lambda i: 3*i+2}, shape=(5,))
>>> ps.evaluate(mask=[1, 3, 4])
>>> ps.as_dict()
{'a': array([ 3, 8, 13]), 'c': array([ 5, 11, 14]), 'b': array([7, 7, 7])}
An example with two-dimensional arrays:
.. doctest::
>>> ps2d = ParameterSpace({'a': [[2, 3, 5, 8, 13], [21, 34, 55, 89, 144]],
... 'b': 7,
... 'c': lambda i, j: 3*i-2*j}, shape=(2, 5))
>>> ps2d.evaluate(mask=(slice(None), [1, 3, 4]))
>>> print(ps2d['a'])
[[ 3 8 13]
[ 34 89 144]]
>>> print(ps2d['c'])
[[-2 -6 -8]
[ 1 -3 -5]]
There are also several methods to allow iterating over the parameter space in
different ways. A :class:`ParameterSpace` can be viewed as both a :class:`dict`
contaning arrays and as an array of dicts. Iterating over a parameter space
gives the latter view:
.. doctest::
>>> for D in ps:
... print(D)
...
{'a': 3, 'c': 5, 'b': 7}
{'a': 8, 'c': 11, 'b': 7}
{'a': 13, 'c': 14, 'b': 7}
unlike for a :class:`dict`, where iterating over it gives the keys.
:meth:`items()` works as for a normal :class:`dict`:
.. doctest::
>>> for key, value in ps.items():
... print(key, "=", value)
a = [ 3 8 13]
c = [ 5 11 14]
b = [7 7 7]
Reference
~~~~~~~~~
.. autoclass:: ParameterSpace
:members:
:undoc-members:
.. automethod:: __getitem__
.. automethod:: __iter__
The :class:`Sequence` class
---------------------------
.. autoclass:: Sequence
:members:
:undoc-members:
.. automethod:: __mul__
.. automethod:: __div__
.. _lazyarray: https://lazyarray.readthedocs.org/
.. _`lazyarray performance`: https://lazyarray.readthedocs.org/en/latest/performance.html
|