Parameter handling

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 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 LazyArray objects, and the set of parameters for a model is contained in a dict-like object, ParameterSpace.

The LazyArray class

LazyArray is a PyNN-specific sub-class of a more general class, larray, and most of its functionality comes from the parent class. Full documentation for larray is available in the lazyarray package, but we give here a quick overview.

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

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

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

>>> tau_m.evaluate(simplify=True)
20.0

Similarly, we can evaluate v_thresh to get a normal NumPy array:

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

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

class LazyArray(value, shape=None, dtype=None)[source]

Bases: larray

Optimises storage of arrays in various ways:
  • stores only a single value if all the values in the array are the same

  • if the array is created from a RandomDistribution or a function f(i,j), then elements are only evaluated when they are accessed. Any operations performed on the array are also queued up to be executed on access.

The main intention of the latter is to save memory for very large arrays by accessing them one row or column at a time: the entire array need never be in memory.

Arguments:
value:

may be an int, float, bool, NumPy array, iterator, generator or a function, f(i) or f(i,j), depending on the dimensions of the array. f(i,j) should return a single number when i and j are integers, and a 1D array when either i or j or both is a NumPy array (in the latter case the two arrays must have equal lengths).

shape:

a tuple giving the shape of the array, or None

dtype:

the NumPy dtype.

__getitem__(addr: Any) Union[ndarray, int64, float64, int, float]

Return one or more items from the array, as for NumPy arrays.

addr may be a single integer, a slice, a NumPy boolean array or a NumPy integer array.

by_column(mask=None)[source]

Iterate over the columns of the array. Columns will be yielded either as a 1D array or as a single value (for a flat array).

mask: either None or a boolean array indicating which columns should be included.

apply(f: Union[Callable, ufunc]) None

Add the function f(x) to the list of the operations to be performed, where x will be a scalar or a numpy array.

>>> m = larray(4, shape=(2,2))
>>> m.apply(np.sqrt)
>>> m.evaluate()
array([[ 2.,  2.],
       [ 2.,  2.]])
check_bounds(addr: Any) None

Check whether the given address is within the array bounds.

evaluate(simplify: bool = False, empty_val: int = 0) Union[ndarray, float64, bool, int, float]

Return the lazy array as a real NumPy array.

If the array is homogeneous and simplify is True, return a single numerical value.

property is_homogeneous: bool

True if all the elements of the array are the same.

property ncols: int

Size of the second dimension (if it exists) of the array.

property nrows: int

Size of the first dimension of the array.

property shape

Shape of the array

property size: int

Total number of elements in the array.

The ParameterSpace class

ParameterSpace is a dict-like class that contains LazyArray objects.

In addition to the usual dict methods, it has several methods that allow operations on all the lazy arrays within it at once. For example:

>>> 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=[]> 
>>> ps.evaluate()
>>> ps['c']
array([ 2,  5,  8, 11])

the evaluate() method also accepts a mask, in order to evaluate only part of the lazy arrays:

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

>>> 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 ParameterSpace can be viewed as both a dict contaning arrays and as an array of dicts. Iterating over a parameter space gives the latter view:

>>> 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 dict, where iterating over it gives the keys. items() works as for a normal dict:

>>> for key, value in ps.items():
...   print(key, "=", value)
a = [ 3  8 13]
c = [ 5 11 14]
b = [7 7 7]

Reference

class ParameterSpace(parameters, schema=None, shape=None, component=None)[source]

Representation of one or more points in a parameter space.

i.e. represents one or more parameter sets, where each parameter set has the same parameter names and types but the parameters may have different values.

Arguments:
parameters:

a dict containing values of any type that may be used to construct a lazy array, i.e. int, float, NumPy array, RandomDistribution, function that accepts a single argument.

schema:

a dict whose keys are the expected parameter names and whose values are the expected parameter types

component:

optional - class for which the parameters are destined. Used in error messages.

shape:

the shape of the lazy arrays that will be constructed.

__getitem__(name)[source]

x.__getitem__(y) <==> x[y]

__iter__()[source]

Return an array-element-wise iterator over the parameter space.

Each item in the iterator is a dict, containing the same keys as the ParameterSpace. For the ith dict returned by the iterator, each value is the ith element of the corresponding lazy array in the parameter space.

Example:

>>> ps = ParameterSpace({'a': [2, 3, 5, 8], 'b': 7, 'c': lambda i: 3*i+2}, shape=(4,))
>>> ps.evaluate()
>>> for D in ps:
...     print(D)
...
{'a': 2, 'c': 2, 'b': 7}
{'a': 3, 'c': 5, 'b': 7}
{'a': 5, 'c': 8, 'b': 7}
{'a': 8, 'c': 11, 'b': 7}
property shape

Size of the lazy arrays contained within the parameter space

keys() list of PS's keys.[source]
items() an iterator over the (key, value) items of PS.[source]

Note that the values will all be LazyArray objects.

update(**parameters)[source]

Update the contents of the parameter space according to the (key, value) pairs in **parameters. All values will be turned into lazy arrays.

If the ParameterSpace has a schema, the keys and the data types of the values will be checked against the schema.

pop(name, d=None)[source]

Remove the given parameter from the parameter set and from its schema, and return its value.

property is_homogeneous

True if all of the lazy arrays within are homogeneous.

evaluate(mask=None, simplify=False)[source]

Evaluate all lazy arrays contained in the parameter space, using the given mask.

as_dict()[source]

Return a plain dict containing the same keys and values as the parameter space. The values must first have been evaluated.

columns()[source]

For a 2D space, return a column-wise iterator over the parameter space.

property parallel_safe
property has_native_rngs

Return True if the parameter set contains any NativeRNGs

expand(new_shape, mask)[source]

Increase the size of the ParameterSpace.

Existing array values are mapped to the indices given in mask. New array values are set to NaN.

flatten(with_prefix=True)[source]

The Sequence class

class Sequence(value)[source]

Represents a sequence of numerical values.

Arguments:
value:

anything which can be converted to a NumPy array, or another Sequence object.

__mul__(val)

Return a new ArrayParameter in which all values in the original ArrayParameter have been multiplied by val.

If val is itself an array, return an array of ArrayParameter objects, where ArrayParameter i is the original ArrayParameter multiplied by element i of val.

__div__(val)

Return a new ArrayParameter in which all values in the original ArrayParameter have been divided by val.

If val is itself an array, return an array of ArrayParameter objects, where ArrayParameter i is the original ArrayParameter divided by element i of val.