Running simulations with PyNN

Importing PyNN

The simulator used by a PyNN script is determined by which module is imported from the PyNN package, e.g.:

>>> from pyNN.neuron import * 
>>> from pyNN.nest import *  
>>> from pyNN.pcsim import *  
>>> from pyNN.brian import *  

After this line, all PyNN code is independent of the simulator used, although it is possible to include simulator-specific code in the script as well (if simulator-independence is not important to you, or if you are in the process of porting simulator-specific code to pure PyNN code).

Initialising the simulator

Before using any other functions or classes from PyNN, the user must call the setup() function:

>>> setup()

setup() takes various optional arguments: setting the simulation timestep (there is currently no support in the API for variable timestep methods although native simulator code can be used to select this option where the simulator supports it) and setting the minimum and maximum synaptic delays, e.g.:

>>> setup(timestep=0.1, min_delay=0.1, max_delay=10.0)

In previous versions, setup() took a debug argument for configuring logging. To allow more flexibility, configuration of logging must now be done separately. There is a convenience function in the pyNN.utility module to simplify this:

>>> from pyNN.utility import init_logging
>>> init_logging("logfile", debug=True)

or you can configure the Python logging module directly.

Creating neurons

Neurons are created using the Population class, which represents a group of neurons all of the same type (i.e. the same model, although different parameterisations of the same model can be used to model different biological neuron types), e.g.:

>>> p1 = Population(100, IF_curr_alpha, structure=space.Grid2D())

This creates 100 integrate-and-fire neurons with default parameters, distributed on a square grid. IF_curr_alpha is a particular class of IF neuron with alpha-function shaped synaptic currents, that will work with any PyNN simulation engine, whether NEURON, NEST, PCSIM or Brian. IF_curr_alpha is a so-called ‘standard cell’, implemented as a Python class. For more information, see the section StandardCells.

Since we didn’t specify any parameters for the neuron model, the neurons we created above have default parameter values, stored in the default_values of the standard cell class, e.g.:

>>> IF_curr_alpha.default_parameters 
{'tau_refrac': 0.10000000000000001, 'v_thresh': -50.0, 'tau_m': 20.0,
 'tau_syn_E': 0.5, 'v_rest': -65.0, 'cm': 1.0, 'v_reset': -65.0,
 'tau_syn_I': 0.5, 'i_offset': 0.0}

To use different parameter values, use the cellparams argument, e.g.:

>>> p2 = Population(20, IF_curr_alpha, cellparams={'tau_m': 15.0, 'cm': 0.9})

If you try to set a non-existent parameter, or pass an invalid value, PyNN will raise an Exception, e.g.:

>>> p2a = Population(20, IF_curr_alpha, cellparams={'foo': 15.0})
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "/home/andrew/dev/pyNN/neuron/__init__.py", line 113, in __init__
      common.Population.__init__(self, size, cellclass, cellparams, structure, label)
    File "/home/andrew/dev/pyNN/common.py", line 879, in __init__
      self.celltype = cellclass(cellparams)
    File "/home/andrew/dev/pyNN/neuron/standardmodels/cells.py", line 35, in __init__
      cells.IF_curr_alpha.__init__(self, parameters) # checks supplied parameters and adds default
    File "/home/andrew/dev/pyNN/standardmodels/__init__.py", line 60, in __init__
      models.BaseModelType.__init__(self, parameters)
    File "/home/andrew/dev/pyNN/models.py", line 16, in __init__
      self.parameters = self.__class__.checkParameters(parameters, with_defaults=True)
    File "/home/andrew/dev/pyNN/models.py", line 67, in checkParameters
      raise errors.NonExistentParameterError(k, cls, cls.default_parameters.keys())
NonExistentParameterError: foo (valid parameters for <class 'pyNN.neuron.standardmodels.cells.IF_curr_alpha'> are: cm, i_offset, tau_m, tau_refrac, tau_syn_E, tau_syn_I, v_reset, v_rest, v_thresh)

>>> p2b = Population(20, IF_curr_alpha, cellparams={'tau_m': 'bar'})
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "/home/andrew/dev/pyNN/neuron/__init__.py", line 113, in __init__
      common.Population.__init__(self, size, cellclass, cellparams, structure, label)
    File "/home/andrew/dev/pyNN/common.py", line 879, in __init__
      self.celltype = cellclass(cellparams)
    File "/home/andrew/dev/pyNN/neuron/standardmodels/cells.py", line 35, in __init__
      cells.IF_curr_alpha.__init__(self, parameters) # checks supplied parameters and adds default
    File "/home/andrew/dev/pyNN/standardmodels/__init__.py", line 60, in __init__
      models.BaseModelType.__init__(self, parameters)
    File "/home/andrew/dev/pyNN/models.py", line 16, in __init__
      self.parameters = self.__class__.checkParameters(parameters, with_defaults=True)
    File "/home/andrew/dev/pyNN/models.py", line 57, in checkParameters
      raise errors.InvalidParameterValueError(err_msg)
InvalidParameterValueError: For tau_m in IF_curr_alpha, expected <type 'float'>, got <type 'str'> (bar)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "/home/andrew/dev/pyNN/neuron/__init__.py", line 113, in __init__
      common.Population.__init__(self, size, cellclass, cellparams, structure, label)
    File "/home/andrew/dev/pyNN/common.py", line 879, in __init__
      self.celltype = cellclass(cellparams)
    File "/home/andrew/dev/pyNN/neuron/standardmodels/cells.py", line 35, in __init__
      cells.IF_curr_alpha.__init__(self, parameters) # checks supplied parameters and adds default
    File "/home/andrew/dev/pyNN/standardmodels/__init__.py", line 60, in __init__
      models.BaseModelType.__init__(self, parameters)
    File "/home/andrew/dev/pyNN/models.py", line 16, in __init__
      self.parameters = self.__class__.checkParameters(parameters, with_defaults=True)
    File "/home/andrew/dev/pyNN/models.py", line 57, in checkParameters
      raise errors.InvalidParameterValueError(err_msg)
InvalidParameterValueError: For tau_m in IF_curr_alpha, expected <type 'float'>, got <type 'str'> (bar)

You can also give your population a label:

>>> p3 = Population(100, SpikeSourceArray, label="Input Population")

This illustrates all the possible arguments of the Population constructor, with argument names. It creates a 3x4x5 array of IF_cond_alpha neurons, all with a spike threshold set to -55 mV and membrane time constant set to 10 ms:

>>> p4 = Population(size=60, cellclass=IF_cond_alpha,
...                 cellparams={'v_thresh': -55.0, 'tau_m': 10.0, 'tau_refrac': 1.5},
...                 structure=space.Grid3D(3./4, 3./5), label="Column 1")

Since creating neurons on a grid is very common, the grid dimensions can be specified in place of the size, without having to create a structure object, e.g.:

>>> p4a = Population((3,4,5), IF_cond_alpha)
>>> assert p4.size == p4a.size == 60
>>> assert p4.structure == p4a.structure, "%s != %s" % (p4.structure, p4a.structure)

The above examples all use PyNN standard cell models. It is also possible to use simulator-specific models by defining a NativeCellClass, e.g. for NEST:

>>> p5 = Population(20, native_cell_type('iaf_neuron'), cellparams={'tau_m': 15.0, 'C_m': 0.001}) 

This example will work with NEST but not with NEURON, PCSIM or Brian.

Setting parameter values

As well as specifying the parameter values for the neuron models when you create a Population, you can also set or change the values for an existing Population.

Setting the same value for the entire population

To set a parameter for all neurons in the population to the same value, use the set() method, e.g.:

>>> p1.set("tau_m", 20.0)
>>> p1.set({'tau_m':20, 'v_rest':-65})

The first form can be used for setting a single parameter, the second form for setting multiple parameters at once.

Setting the parameters of individual neurons

To address individual neurons in a population, use [] notation, e.g.,:

>>> p1[0]
1
>>> p1[99]
100
>>> p2[17]
118
>>> p3[44]
246

The return values are ID objects, which behave in most cases as integers, but also allow accessing the values of the cell parameters. The value within the square brackets is referred to as a neuron’s index, which always runs from 0 to size-1 for a given population, while the return value is its id. Trying to address a non-existent neuron will raise an Exception:

>>> p1[999]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/andrew/dev/pyNN/common.py", line 394, in __getitem__
    return self.all_cells[index]
IndexError: index out of bounds
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/andrew/dev/pyNN/common.py", line 394, in __getitem__
    return self.all_cells[index]
IndexError: index out of bounds

To obtain an index given the id, use id_to_index(), e.g.:

>>> p3[49]
170
>>> p3.id_to_index(170)
49

The ID object allows direct access to the parameters of individual neurons, e.g.:

>>> p4[0].tau_m
10.0
>>> p4[0].tau_m = 15
>>> p4[0].tau_m
15.0

To change several parameters at once for a single neuron, use the set_parameters() method of the neuron ID, e.g.:

>>> p4[1].set_parameters(tau_m=10.0, cm=0.5)
>>> p4[1].tau_m
10.0
>>> p4[1].cm
0.5

Setting the parameters of a subset of neurons

To access several neurons at once, use slice notation, e.g., to access the first five neurons in a population, use:

>>> p2[:5]

A PopulationView holds references to a subset of neurons in a Population, which means that any changes in the view are also reflected in the real population (and vice versa), e.g.:

>>> view = p2[:5]
>>> view.set('tau_m', 11.0)
>>> p2.get('tau_m')
[11.0, 11.0, 11.0, 11.0, 11.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0, 15.0]

PopulationView objects behave in most ways as real Population objects, notably, they can be used in a Projection (see below) and combined with other Population or PopulationView objects to create an Assembly.

Setting random values

To set a parameter to values drawn from a random distribution, use the rset() method with a RandomDistribution object from the pyNN.random module (see the chapter on random numbers for more details). The following example sets the threshold potential of each neuron to a value drawn from a uniform distribution between -55 mV and -50 mV:

>>> from pyNN.random import RandomDistribution
>>> vthresh_distr = RandomDistribution(distribution='uniform', parameters=[-55,-50])
>>> p1.rset('v_thresh', vthresh_distr)

Note that positional arguments can also be used. The following produces the same result as the above:

>>> vthresh_distr = RandomDistribution('uniform', [-55,-50])

Setting values according to an array

The most efficient way to set different (but non-random) values for different neurons is to use the tset() (for topographic set) method. The following example injects a current of 0.1 nA into the first 20 neurons in the population:

>>> import numpy
>>> current_input = numpy.zeros(p1.size)
>>> current_input[:20] = 0.1
>>> p1.tset('i_offset', current_input)

Setting initial values

To set the initial values of state variable such as the membrane potential use the initialize() method:

>>> p1.initialize('v', -65.0)

To initialize different neurons to different random values, pass a RandomDistribution object instead of a float:

>>> vinit_distr = RandomDistribution(distribution='uniform', parameters=[-70,-60])
>>> p1.initialize('v', vinit_distr)

Iterating over all the neurons in a population

To iterate over all the cells in a population, returning the neuron ids, use:

>>> for id in p2: 
...   print id, id.tau_m
...
100 11.0
101 11.0
102 11.0
103 11.0
104 11.0
105 15.0
106 15.0
...

Injecting current

Static or time-varying currents may be injected into the cells of a Population using either the inject_into() method of the CurrentSource or the inject() method of the Population:

>>> pulse = DCSource(amplitude=0.5, start=20.0, stop=80.0)
>>> pulse.inject_into(p1[3:7])
>>> p4.inject(pulse)

>>> times = numpy.arange(0.0, 100.0, 1.0)
>>> amplitudes = 0.1*numpy.sin(times*numpy.pi/100.0)
>>> sine_wave = StepCurrentSource(times, amplitudes)
>>> p1[(6,11,27)].inject(sine_wave)
>>> sine_wave.inject_into(p5)  
>>> sine_wave.inject_into(p3)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
      sine_wave.inject_into(p3)
    File "/usr/lib/python/site-packages/pyNN/neuron/electrodes.py", line 67, in inject_into
      raise TypeError("Can't inject current into a spike source.")
TypeError: Can't inject current into a spike source.
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
      sine_wave.inject_into(p3)
    File "/usr/lib/python/site-packages/pyNN/neuron/electrodes.py", line 67, in inject_into
      raise TypeError("Can't inject current into a spike source.")
TypeError: Can't inject current into a spike source.

Recording

Recording spike times is done with the method record(). Recording membrane potential is done with the method record_v(). Recording synaptic conductances is done with the method record_gsyn(). All three methods have identical argument lists. Some examples:

>>> p1.record()                # record from all neurons in the population
>>> p1.sample(10).record()     # record from 10 neurons chosen at random
>>> p1[[0,1,2]].record()       # record from specific neurons

Running a simulation

The run() function runs the simulation for a given number of milliseconds, e.g.:

>>> run(1000.0)

Accessing recorded data and writing to file

Writing recorded values to file is done with a second triple of methods, printSpikes(), print_v() and print_gsyn(), e.g.:

>>> p1.printSpikes("spikefile.dat")

By default, the output files are post-processed to reformat them from the native simulator format to a common format that is the same for all simulator engines.

The beginning of a typical spike file looks like:

# dt = 0.1
# n = 1000
0.0     2
0.3     5
0.4     3
0.9     2
1.0     1
. . .

The beginning of a typical membrane potential file looks like:

# dt = 0.1
# n = 1000
-65.0   0
-64.9   0
-64.7   0
-64.5   0
. . .

Both file types begin with header lines giving the timestep (there is currently no support for variable-time step recording) and the number of data points in the file. Each line of the spike file then gives the occurence time of a spike (in ms) and the id of the neuron in which it was recorded. Each line of the membrane potential file gives the membrane potential (in mV) followed by the id of the neuron in which it was recorded. In both cases, whether the file is sorted by cell id or by time depends on the simulator: it is not standardised.

Having a standard format facilitates comparisons across simulators, but of course has some performace penalty. To get output in the native format of the simulator, add compatible_output=False to the argument list.

When running a distributed simulation, each node records only those neurons that it simulates. By default, at the end of the simulation all nodes send their recorded data to the master node so that all values are written to a single output file. Again, there is a performance penalty for this, so if you wish each node to write its own file, add gather=False to the argument list.

It is possible to save data in various different formats. The default (if you pass a filename) is a text file, but you can also save in various binary formats. To save in HDF5 format (this requires the PyTables package to be installed), for example:

>>> from pyNN.recording.files import HDF5ArrayFile
>>> h5file = HDF5ArrayFile("spikefile.h5", "w")
>>> p1.printSpikes(h5file)
>>> h5file.close()

If you wish to obtain the recorded data within the simulation script, for plotting or further analysis, there is a further triple of methods, getSpikes(), get_v() and get_gsyn(). Again, there is a gather option for distributed simulations.

Statistics of recorded data

Often, the exact spike times and exact membrane potential traces are not required, only statistical measures. PyNN currently only provides one such measure, the mean number of spikes per neuron, e.g.:

>>> p1.meanSpikeCount()
0.01

More such statistical measures are planned for future releases. # mention p1.get_spike_counts()

Repeating a simulation

If you wish to reset network time to zero to run a new simulation with the same network (with different parameter values, perhaps), use the reset() function. Note that this does not change the network structure, nor the choice of which neurons to record (from previous record() calls).

Position in space

The positions of neurons in space are usually determined by passing in a Structure object when creating a Population (see above). Some examples:

>>> s1 = space.Line()
>>> s1.generate_positions(3)
 array([[ 0.,  1.,  2.],
        [ 0.,  0.,  0.],
        [ 0.,  0.,  0.]])
>>> s2 = space.Grid2D(aspect_ratio=2.0, dx=3.0, dy=7.0)
>>> s2.generate_positions(8)
array([[ 0.,  0.,  3.,  3.,  6.,  6.,  9.,  9.],
       [ 0.,  7.,  0.,  7.,  0.,  7.,  0.,  7.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.]])
>>> s3 = space.RandomStructure(boundary=space.Sphere(radius=100.0))
>>> s3.generate_positions(5)
 array([[ 15.7103484 ,   7.56979681, -26.39920966, -81.56024563, -27.30566837],
       [ 62.74380383, -26.29395986,  33.23787658, -16.46650874, -79.4064587 ],
       [  5.66080048, -70.5696085 , -42.68101409, -55.20377865, -24.22675542]])
>>> s4 = space.RandomStructure(boundary=space.Cuboid(30,40,50))
>>> s4.generate_positions(5)
array([[ -2.69455996,   9.04858685,  -4.61756624,   4.53035932, -4.80972742],
       [ 12.946409  ,  11.31629902,   5.52137332,   5.49659371, -6.80003331],
       [ -9.36346794, -23.62104418,  -6.2160148 , -15.16040818, -9.66371093]])

The positions of individual neurons in a population can be accessed using their position attribute, e.g.:

>>> p1[99].position = (0.0, 9.0, 9.0)
>>> p1[99].position
array([ 0.,  9.,  9.])

Positions are always in 3D, and may be given as integers or floating-point values, and as tuples or as numpy arrays. No specific coordinate system or scale of units is assumed, although many parts of PyNN do assume a Euclidean coordinate system.

To obtain the positions of all neurons at once (as a numpy array), use the positions attribute of the Population object, e.g.:

>>> p1.positions 
array([[...]])

To find the neuron that is closest to a particular point in space, use the nearest() attribute:

>>> p1.nearest((4.5, 7.8, 3.3))
49
>>> p1[p1.id_to_index(49)].position
array([ 4.,  8.,  0.])

Getting information about a Population

A summary of the state of a Population may be obtained with the describe() method:

>>> print p4.describe() 
Population "Column 1"
    Structure   : Grid3D
      aspect_ratios: (0.75, 0.59999999999999998)
      fill_order: sequential
      dz: 1.0
      dx: 1.0
      dy: 1.0
      y0: 0.0
      x0: 0.0
      z0: 0
    Local cells : 60
    Cell type   : IF_cond_alpha
    ID range    : 221-280
    First cell on this node:
      ID: 221
      tau_refrac: 1.5
      tau_m: 15.0
      e_rev_E: 0.0
      i_offset: 0.0
      cm: 1.0
      e_rev_I: -70.0
      v_thresh: -55.0
      tau_syn_E: 0.3
      v_rest: -65.0
      tau_syn_I: 0.5
      v_reset: -65.0

The output format can be customized by passing a Jinja or Cheetah template:

>>> print p4.describe(template="Population of {{size}} {{celltype.name}} cells",
...                   engine='jinja2')
Population of 60 IF_cond_alpha cells

where template can be a string or a filename.

Connecting neurons

A Projection object is a container for all the synaptic connections between neurons in two Populations (or PopulationView``s, or ``Assembly``s (see below)), together with methods for setting synaptic weights, delays and other properties. A ``Projection is created by specifying a pre-synaptic Population, a post-synaptic Population and a Connector object, which determines the algorithm used to wire up the neurons, e.g.:

>>> prj2_1 = Projection(p2, p1, method=AllToAllConnector(), target='excitatory')

This connects p2 (pre-synaptic) to p1 (post-synaptic) with excitatory synapses, using an ‘AllToAllConnector‘ object, which connects every neuron in the pre-synaptic population to every neuron in the post-synaptic population. The currently available Connector classes are explained below. It is fairly straightforward for a user to write a new Connector class if they wish to use a connection algorithm not already available in PyNN.

Note that the attribute synapse_types of all standard-cell classes contains a list of the possible values of target for that cell type.

All-to-all connections

The AllToAllConnector' constructor has one optional argument allow_self_connections, for use when connecting a Population to itself. By default it is True, but if a neuron should not connect to itself, set it to False, e.g.:

>>> prj1_1 = Projection(p1, p1, AllToAllConnector(allow_self_connections=False))

One-to-one connections

Use of the OneToOneConnector requires that the pre- and post-synaptic populations have the same size, e.g.:

>>> prj1_1a = Projection(p1, p1, OneToOneConnector())

Trying to connect two Populations with different sizes will raise an Exception, e.g.:

>>> invalid_prj = Projection(p2, p3, OneToOneConnector()) 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    invalid_prj = Projection(p2, p3, OneToOneConnector())
  File "/usr/lib/python/site-packages/pyNN/neuron/__init__.py", line 220, in __init__
    method.connect(self)
  File "/usr/lib/python/site-packages/pyNN/connectors.py", line 281, in connect
    raise common.InvalidDimensionsError("OneToOneConnector does not support
               presynaptic and postsynaptic Populations of different sizes.")
InvalidDimensionsError: OneToOneConnector does not support presynaptic and
                        postsynaptic Populations of different sizes.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    invalid_prj = Projection(p2, p3, OneToOneConnector())
  File "/usr/lib/python/site-packages/pyNN/neuron/__init__.py", line 220, in __init__
    method.connect(self)
  File "/usr/lib/python/site-packages/pyNN/connectors.py", line 281, in connect
    raise common.InvalidDimensionsError("OneToOneConnector does not support
               presynaptic and postsynaptic Populations of different sizes.")
InvalidDimensionsError: OneToOneConnector does not support presynaptic and

Connecting neurons with a fixed probability

With the FixedProbabilityConnector method, each possible connection between all pre-synaptic neurons and all post-synaptic neurons is created with probability p_connect, e.g.:

>>> prj3_2 = Projection(p3, p2, FixedProbabilityConnector(p_connect=0.2))

The constructor also accepts an allow_self_connections parameter, as above.

Connecting neurons with a distance-dependent probability

For each pair of pre-post cells, the connection probability depends on distance (see above for how to specify neuron positions in space).

The constructor requires a string d_expression, which should be the right-hand side of a valid Python expression for probability (i.e. returning a value between 0 and 1), involving ‘d‘, e.g.:

>>> prj1_1b = Projection(p1, p1, DistanceDependentProbabilityConnector("exp(-d)"))
>>> prj2_2  = Projection(p2, p2, DistanceDependentProbabilityConnector("d<3"))

The first example connects neurons with an exponentially-decaying probability. The second example connects each neuron to all its neighbours within a range of 3 units (typically interpreted as µm, but this is up to the individual user). Note that boolean values True and False are automatically converted to numerical values 1.0 and 0.0.

The calculation of distance may be controlled by specifying a Space object.

By default, the 3D distance between cell positions is used, but the axes argument may be used to change this, e.g.:

>>> connector = DistanceDependentProbabilityConnector("exp(-abs(d))", space=Space(axes='xy'))

will ignore the z-coordinate when calculating distance.

Similarly, the origins of the coordinate systems of the two Population``s and the relative scale of the two coordinate systems may be controlled using the ``offset and scale_factor arguments to the Space constructor. This is useful when connecting brain regions that have very different sizes but that have a topographic mapping between them, e.g. retina to LGN to V1.

In more abstract models, it is often useful to be able to avoid edge effects by specifying periodic boundary conditions, e.g.:

>>> connector = DistanceDependentProbabilityConnector("exp(-abs(d))", space=Space(periodic_boundaries=((0,500), (0,500), None)))

calculates distance on the surface of a torus of circumference 500 µm (wrap-around in the x- and y-dimensions but not z).

Divergent/fan-out connections

The FixedNumberPostConnector connects each pre-synaptic neuron to exactly n post-synaptic neurons chosen at random:

>>> prj2_1a = Projection(p2, p1, FixedNumberPostConnector(n=30))

As a refinement to this, the number of post-synaptic neurons can be chosen at random from a RandomDistribution object, e.g.:

>>> distr_npost = RandomDistribution(distribution='binomial', parameters=[100,0.3])
>>> prj2_1b = Projection(p2, p1, FixedNumberPostConnector(n=distr_npost))

Convergent/fan-in connections

The FixedNumberPreConnector has the same arguments as FixedNumberPostConnector, but of course it connects each post-synaptic neuron to n pre-synaptic neurons, e.g.:

>>> prj2_1c = Projection(p2, p1, FixedNumberPreConnector(5))
>>> distr_npre = RandomDistribution(distribution='poisson', parameters=[5])
>>> prj2_1d = Projection(p2, p1, FixedNumberPreConnector(distr_npre))

Using the Connection Set Algebra

The Connection Set Algebra (Djurfeldt, 2010) is a sophisticated system that allows elaborate connectivity patterns to be constructed using a concise syntax. Using the CSA requires the Python csa module to be installed (see Installation).

The details of constructing a connection set are beyond the scope of this manual. We give here a simple example.

>>> import csa
>>> cs = csa.full - csa.oneToOne
>>> prj2_1e = Projection(p2, p1, CSAConnector(cs))

Writing and reading connection patterns to/from a file

Connection patterns can be written to a file using saveConnections(), e.g.:

>>> prj1_1a.saveConnections("prj1_1a.conn")

These files can then be read back in to create a new Projection object using a FromFileConnector object, e.g.:

>>> prj1_1c = Projection(p1, p1, FromFileConnector("prj1_1a.conn"))

Specifying a list of connections

Specific connection patterns not covered by the methods above can be obtained by specifying an explicit list of pre-synaptic and post-synaptic neuron indices, with weights and delays. (Note that the weights and delays should be optional, but currently are not). Example:

>>> conn_list = [
...   (0, 0, 0.0, 0.1),
...   (0, 1, 0.0, 0.1),
...   (0, 2, 0.0, 0.1),
...   (1, 5, 0.0, 0.1)
... ]
>>> prj1_2d = Projection(p1, p2, FromListConnector(conn_list))

User-defined connection algorithms

If you wish to use a specific connection/wiring algorithm not covered by the PyNN built-in ones, the options include: constructing a list of connections and use the FromListConnector class; using the CSA (see above); writing your own Connector class (by looking at the code for the built-in Connectors, this should be quite straightforward).

Setting synaptic weights and delays

Synaptic weights and delays may be set either when creating the Projection, as arguments to the Connector object, or afterwards using the setWeights() and setDelays() methods Projection.

All Connector objects (except CSAConnector) accept weights and delays arguments to their constructors. Some examples:

To set all weights to the same value:

>>> connector = AllToAllConnector(weights=0.7)
>>> prj1_2e = Projection(p1, p2, connector)

To set delays to random values taken from a specific distribution:

>>> delay_distr = RandomDistribution(distribution='gamma',parameters=[5,0.5])
>>> connector = FixedProbabilityConnector(p_connect=0.5, delays=delay_distr)
>>> prj2_1e = Projection(p2, p1, connector)

To set individual weights and delays to specific values:

>>> weights = numpy.arange(1.1, 2.0, 0.9/p1.size)
>>> delays = 2*weights
>>> connector = OneToOneConnector(weights=weights, delays=delays)
>>> prj1_1d = Projection(p1, p1, connector)

After creating the Projection, to set the weights of all synaptic connections in a Projection to a single value, use the setWeights() method:

>>> prj1_1.setWeights(0.2)

[Note: synaptic weights in PyNN are in nA for current-based synapses and µS for conductance-based synapses)].

To set different weights to different values, use setWeights() with a list or 1D numpy array argument, where the length of the list/array is equal to the number of synapses, e.g.:

>>> weight_list = 0.1*numpy.ones(len(prj2_1))
>>> weight_list[0:5] = 0.2
>>> prj2_1.setWeights(weight_list)

To set weights to random values, use the randomizeWeights() method:

>>> weight_distr = RandomDistribution(distribution='gamma',parameters=[1,0.1])
>>> prj1_1.randomizeWeights(weight_distr)

Setting delays works similarly:

>>> prj1_1.setDelays(0.6)

>>> delay_list = 0.3*numpy.ones(len(prj2_1))
>>> delay_list[0:5] = 0.4
>>> prj2_1.setDelays(delay_list)
>>> delay_distr = RandomDistribution(distribution='gamma', parameters=[2,0.2], boundaries=[get_min_delay(),1e12])
>>> prj1_1.randomizeDelays(delay_distr)

It is also possible to access the attributes of individual connections using the connections attribute of a projection:

>>> for c in prj1_1.connections[:5]:
...   c.weight *= 2

In general, though, this is less efficient than using list- or array-based access.

For the CSAConnector, weights and delays may be specified as part of the connection set - see the CSA documentation for details.

Accessing weights and delays

To get the weights of all connections in the Projection, use the getWeights() method. Two formats are available. 'list' returns a list of length equal to the number of connections in the projection, 'array' returns a 2D weight array (with NaN for non-existent connections):

>>> prj2_1.getWeights(format='list')[3:7]
[0.20000000000000001, 0.20000000000000001, 0.10000000000000001, 0.10000000000000001]
>>> prj2_1.getWeights(format='array')[:3,:3]
array([[ 0.2,  0.2,  0.2],
       [ 0.1,  0.1,  0.1],
       [ 0.1,  0.1,  0.1]])

getDelays() is analogous. printWeights() writes the weights to a file.

Access to the weights and delays of individual connections is by the connections attribute, e.g.:

>>> prj2_1.connections[0].weight
0.2
>>> prj2_1.connections[10].weight
0.1

The weightHistogram() method returns a histogram of the synaptic weights, with bins determined by the min, max and nbins arguments passed to the method.

Getting information about a Projection

As for Population, a summary of the state of a Projection may be obtained with the describe() method:

>>> print prj2_1.describe()
Projection "population1→population0" from "population1" (20 cells) to "population0" (100 cells)
Target     : excitatory
Connector  : AllToAllConnector
    allow_self_connections : True
    Weights : 0.0
    Delays : 0.1
Plasticity :
        None
Total connections : 2000
Local connections : 2000

Synaptic plasticity

So far we have discussed only the case whether the synaptic weight is fixed. Dynamic synapses (short-term and long-term plasticity) are discussed in the next chapter.

Higher-level structures

As we noted above, the neurons in a Population all have the same type. To simplify working with many different populations, individual Population and PopulationView objects may be aggregated in an Assembly, e.g.:

>>> assembly = Assembly(p1, p2[10:15], p3, p4)

An assembly behaves in many ways like a Population, e.g. setting and retrieving parameters, specifying which neurons to record from, etc. It can also be specified as the source or target of a Projection. In this case, all the neurons in the component populations are treated as identical for the purposes of the connection algorithm (note that if the synapse type is specified (with the target attribute), an Exception will be raised if not all component neuron types possess that synapse type).

You can also create an assembly simply by adding multiple different Population or PopulationView objects together:

>>> another_assembly = p3 + p4

Individual populations within an Assembly may be accessed via their labels, e.g.

>>> assembly.get_population("Input Population")
<pyNN.nest.Population object at 0x4be0250>
>>> assembly.get_population("Column 1")
<pyNN.nest.Population object at 0x4be0390>

Iterating over an assembly returns individual IDs, ordered by population. Similarly, the size attribute of an Assembly gives the total number of neurons it contains. To iterate over or count populations, use the populations attribute:

>>> for p in assembly.populations:
...    print p.label, p.size, p.celltype
population0 100 IF_curr_alpha
view of population1 with mask slice(10, 15, None) 5 IF_curr_alpha
Input Population 100 SpikeSourceArray
Column 1 60 IF_cond_alpha

Finishing up

Just as a simulation must be begun with a call to setup(), it must be ended with a call to end().

Examples

There are several example scripts in the examples directory of the source distribution.