# Accessing multiple neurons with device groups¶

In some applications, particularly when working with image data, it is not sufficient to work with single neurons. This may work with the simplified Braitenberg network because it only has a fixed amount of sensor neurons regardless of the image resolution. However, this is different in the general case when an image should be processed by hundreds or thousands of neurons. In this case, it is infeasible to create either hundreds of transfer functions that essentially do the same or have a single TF with hundreds of parameters.

For this reason, device groups have been created. That is, a set of devices of the same kind (such as a thousand Poisson generators) can be accessed through a device group. Such a device group always has the same properties as the original device type but returns numpy arrays instead of simple values. Similarly, when an attribute is assigned a value and that value is a numpy array, the values are distributed to the devices accordingly. A device group also accepts a single value to be assigned to a property. In that case, this value is assigned to the respective property for all devices.

To create a device group in Python, two ways are supported. You can either map a population of neurons to devices to attach them to, or create chains of neurons. Alternatively, you can use a combination of both.

@nrp.MapSpikeSink("left_wheel_neuron", nrp.brain.actors[0], nrp.leaky_integrator_alpha)
@nrp.MapSpikeSink("right_wheel_neuron", nrp.brain.actors[1], nrp.leaky_integrator_alpha)


the following code will create a device group with both neurons:

@nrp.MapSpikeSink("wheel_neurons", nrp.chain_neurons(nrp.brain.actors[0], nrp.brain.actors[1]), nrp.leaky_integrator_alpha)


What this assignment will do is that it tells the TF Framework that the parameter wheel_neurons should be mapped to an array of leaky integrators. This array should stack a leaky integrator for nrp.brain.actors[0] and nrp.brain.actors[1]. Within the function, the device group allows two different modes of interaction, either by accessing the devices by array indices or by directly accessing the properties from the device group. In that case, the device group automatically collects the properties from the devices into a numpy array.

# accessing the device for the left wheel by index
# left_wheel_voltage is now a double
left_wheel_voltage = wheel_neurons[0].voltage
# accessing the device voltages from all devices in the device group
# wheel_voltage is now a numpy array
wheel_voltages = wheel_neurons.voltage
# since the TF framework caches the voltages, they are equal
assert left_wheel_voltage == wheel_voltages[0]


Conversely, the devices of a device group can also be configured together by passing a numpy array as an argument with the same length as there are devices in the device group. Alternatively, it is allowed to specify a single value. In this case, this value is passed to all devices in this device group.

@nrp.MapSpikeSource("neurons", nrp.map_neurons(range(0,5), lambda i: nrp.brain.sensors[i]), nrp.poisson)
def test_device_groups(t, neurons):
# We can assign the rates of each poisson generator individually
neurons[0].rate = 42.0
# Alternatively, we can configure all devices at once using a numpy array
neurons.rate = numpy.array([1.0, 1.1, 1.2, 1.3, 1.4])
# Instead of a numpy array, we could also have used anything that is indexable as well, such as a list
neurons.rate = [1.0, 1.1, 1.2, 1.3, 1.4]
# If we wanted to set the rates for all devices to an equal rate, we can also use a single value
# This will set the rates to 42.0 for all devices
neurons.rate = 42.0


In many cases the size of a population to connect to is too large or even unknown when writing the transfer functions. In that case, we can map a collection such as a range to neuron connections to fulfill the same goal.

@nrp.MapSpikeSink("wheel_neurons", nrp.map_neurons(range(0,2), lambda i: nrp.brain.actors[i]), nrp.leaky_integrator_alpha)


The intention behind this assignment is that the function nrp.map_neurons maps an index set to a list of neurons the neuronal simulation should connect to. The lambda expression may either return a single neuron specification or a list. In the latter case, the resulting lists get concatenated so that the function can be used to specify patterns.