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