=============================
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 * #doctest: +SKIP
    >>> from pyNN.nest import *  #doctest: +SKIP
    >>> from pyNN.pcsim import *  #doctest: +SKIP
    >>> from pyNN.brian import *  #doctest: +SKIP
    
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 #doctest: +NORMALIZE_WHITESPACE
    {'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)
    
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}) #doctest: +SKIP
    
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

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: #doctest: +ELLIPSIS
    ...   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)  #doctest: +SKIP
    >>> 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.
    
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 #doctest: +ELLIPSIS,+NORMALIZE_WHITESPACE
    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() #doctest: +NORMALIZE_WHITESPACE
    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()) #doctest: +IGNORE_EXCEPTION_DETAIL
    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.
    
    
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.

.. _`CSA documentation`: http://software.incf.org/software/csa/
.. _`jinja`: http://jinja.pocoo.org/
.. _`cheetah`: http://www.cheetahtemplate.org/