Building models

The Neuromorphic Computing Platform executes experiments performed on computational models of neuronal networks. Both the experiment description and the model description must be written as Python scripts, using the PyNN application programming interface (API).

The PyNN model description API

PyNN is a Python package which defines an API for defining neuronal network models with spiking neurons in a simulator-independent way. There are implementations of this API for the NEST, NEURON and Brian simulators and for both of the HBP Neuromorphic Computing systems (PM and MC/SpiNNaker). In this documentation, we refer to each of these simulators and hardware platforms as a PyNN “backend”.

Full documentation of the API is available at:

At the time of writing, both the PM and MC systems implement version 0.7 of the API, while the simulator of the PM hardware (ESS) supports both versions 0.7 and 0.8, and the chip-based PM hardware (Spikey) supports version 0.6.

A simple example

We present here a simple example network, a toy model of a “synfire chain”, in which the activity propagates across the network.

As with any Python script, the first step is to import the external libraries that we will use. Here we import the NEST backend of PyNN for the simulation, NumPy for random number generation, and matplotlib for plotting.


import pyNN.nest as sim
import numpy.random
import matplotlib.pyplot as plt


Next we define numerical parameters, such as the number and size of neuronal populations, neuron properties, synaptic weights and delays, etc.


n_populations = 11
population_size = 8
neuron_parameters = {
    'cm': 0.2,
    'v_reset': -70,
    'v_rest': -70,
    'v_thresh': -47,
    'e_rev_I': -70,
    'e_rev_E': 0.0,
}
weight_exc_exc = 0.005
weight_exc_inh = 0.005
weight_inh_exc = 0.5
delay = 3.0
rng_seed = 42
stimulus_onset = 25.0
stimulus_sigma = 0.5
runtime = 150.0


The setup() function initializes and configures the simulator (in this case NEST).


sim.setup(timestep=0.1)


The main building block in PyNN is a population of neurons of the same type (although the parameters of the neurons within the population can be heterogeneous). Here we create 11 populations of excitatory integrate-and-fire (IF) neurons and 11 populations of inhibitory IF neurons.


populations = {'exc': [], 'inh': []}
for syn_type in ('exc', 'inh'):
    populations[syn_type] = [sim.Population(population_size,
                                            sim.IF_cond_exp,
                                            neuron_parameters)
                             for i in range(n_populations)]


We now connect each excitatory population to the following pair of excitatory and inhibitory populations, and each inhibitory population to the excitatory population within the same pair.


connector_exc_exc = sim.AllToAllConnector(weights=weight_exc_exc, delays=delay)
connector_exc_inh = sim.AllToAllConnector(weights=weight_exc_inh, delays=delay)
connector_inh_exc = sim.AllToAllConnector(weights=weight_inh_exc, delays=delay)

for i in range(n_populations):
    j = (i + 1) % n_populations
    prj_exc_exc = sim.Projection(populations['exc'][i], populations['exc'][j],
                                 connector_exc_exc, target='excitatory')
    prj_exc_inh = sim.Projection(populations['exc'][i], populations['inh'][j],
                                 connector_exc_inh, target='excitatory')
    prj_inh_exc = sim.Projection(populations['inh'][i], populations['exc'][i],
                                 connector_exc_exc, target='inhibitory')

The first pair of populations are stimulated by a burst of spikes.


numpy.random.seed(rng_seed)
stim_spikes = numpy.random.normal(loc=stimulus_onset,
                                  scale=stimulus_sigma,
                                  size=population_size)
stim_spikes.sort()
stimulus = sim.Population(1, sim.SpikeSourceArray, {'spike_times': stim_spikes})

prj_stim_exc = sim.Projection(stimulus, populations['exc'][0],
                              connector_exc_exc, target='excitatory')
prj_stim_inh = sim.Projection(stimulus, populations['inh'][0],
                              connector_exc_inh, target='excitatory')


We’ve now finished building the network, now we instrument it, by recording spikes from all populations.


for syn_type in ('exc', 'inh'):
    for population in populations[syn_type]:
        population.record()


Now we run the simulation:


sim.run(runtime)


Finally we loop over the populations, retrieve the spike times, and plot a raster plot (spike time vs neuron index).


colours = {'exc': 'r', 'inh': 'b'}

id_offset = 0
for syn_type in ['exc', 'inh']:
    for population in populations[syn_type]:
        spikes = population.getSpikes()
        colour = colours[syn_type]
        plt.plot(spikes[:,1], spikes[:,0] + id_offset,
                 ls='', marker='o', ms=1, c=colour, mec=colour)
        id_offset += population.size

plt.xlim((0, runtime))
plt.ylim((-0.5, 2* n_populations * population_size + 0.5))
plt.xlabel('time (t)')
plt.ylabel('neuron index')
plt.savefig("synfire_chain.png")
_images/synfire_chain.png

Note that here we include the plotting in the same script just to illustrate the example output. Scripts submitted to the Neuromorphic Computing Platform should not in general contain graph plotting or data analysis code. Instead you should save spikes to file and use the other tools available in the Collaboratory for analysing and visualizing the results.

Using different backends

To run the same simulation with a different simulator, just change the name of the PyNN backend to import, e.g.:

import pyNN.neuron as sim

to run the simulation with NEURON. For the PM system, the module to import is pyNN.hardware.hbp_pm while for the MC system the module is pyNN.spiNNaker.

The recommended approach is to provide the name of the backend as a command-line argument, i.e., you run your simulation using:

$ python run.py nest

PyNN contains some utility functions to make this easier. With PyNN 0.7, use:

from pyNN.utility import get_script_args
simulator_name = get_script_args(1)[0]
exec("import pyNN.%s as sim" % simulator_name)

With PyNN 0.8:

from pyNN.utility import get_simulator
sim, options = get_simulator()

“Physical model” (BrainScaleS) system

The BrainScaleS system has a number of additional configuration options that can be passed to setup(). These are explained in About the BrainScaleS hardware. There are also a number of limitations, for example only a subset of the PyNN standard neuron and synapse models are available.

The BrainScaleS system attempts to automatically place neurons on the wafers in an optimal way. However, it is possible to influence this placement or control it manually. An example can be found in label-marocco-example.

“Many core” (SpiNNaker) system

The SpiNNaker system supports the standard arguments provided by the PyNN setup() function. The SpiNNaker system has a number of limitations in terms of the support for PyNN functionality, for example, only a subset of the PyNN standard neuron and synapse models are currently available. These limitations are defined in the online documentation here.

The SpiNNaker software stack attempts to automatically partition the populations defined in the PyNN script into core sized chunks (the smallest atomic size of resource for a machine) of neurons which are then placed onto the machine in an optimal way in regard to the machine’s available resources. However, it is possible to influence the partitioning and placement behaviours manually.

For example, take the PyNN definition of a population from the “synfire chain” example discussed previously, and shown below:

populations[syn_type] = sim.Population(population_size,
                                       sim.IF_cond_exp,
                                       neuron_parameters)

A partitioning limitation/constraint can be added to the population, which can limit how many neurons each core size chunk will contain at a maximum. This is shown below:

populations[syn_type] = sim.Population(population_size,
                                       sim.IF_cond_exp,
                                       neuron_parameters)
populations[syn_type].add_constraint(sim.PartitionerMaximumSizeConstraint(200))

This and other examples of manual limitations can be found in the online documentation here.