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 Population
s (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 Population
s 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 Connector
s, 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.