================== 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'] 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