Building networks: connections

Conceptually, a synapse consists of a pre-synaptic structure, the synaptic cleft, and a post-synaptic structure. In PyNN, the temporal dynamics of the post-synaptic response are handled by the post-synaptic neuron model (see Cell types). The size of the post-synaptic response (the “synaptic weight”), the temporal dynamics of the weight (synaptic plasticity) and the connection delay are handled by synapse models.

At the time of writing, most neuronal network models do not explicitly model the axon. Rather, the time for propagation of the action potential from soma/initial segment to axon terminal is added to the synaptic transmission time to give a composite delay, referred to as “synaptic delay” in this documentation. For point neuron models, which do not include an explicit model of the dendrite, the time for transmission of the post-synaptic potential to the soma may also be considered as being included in the composite synaptic delay.

At a minimum, therefore, a synaptic connection in PyNN has two attributes: “weight” and “delay”, which are interpreted as described above. Where the weight has its own dynamics, a connection may have more attributes: the plasticity model and its parameters.

Note

Currently, PyNN supports only chemical synapses, not electrical synapses. If the underlying simulator supports electrical synapses, it is still possible to use them in a PyNN model, but this will not be simulator-independent.

Note

Currently, PyNN does not support stochastic synapses. If you would like to have support for this, or any other feature, please make a feature request.

Synapse types

Analogously to neuron models, the system of equations that defines a synapse model is encapsulated in a SynapseType class. PyNN provides a library of “standard” synapse types (see Standard models) which work the same across all backend simulators.

Fixed synaptic weight

The simplest, and default synapse type in PyNN has constant synaptic weight:

syn = StaticSynapse(weight=0.04, delay=0.5)

Note

weights are in microsiemens or nanoamps, depending on whether the post-synaptic mechanism implements a change in conductance or current, and delays are in milliseconds (see Units). Weights should always be positive, except for the case of inhibitory (see receptor_type argument below), current-based synapses, for which they should be negative. Inhibitory, conductance-based synapses have positive weights, because it is the reversal potential which makes it inhibitory.

It is also possible to add variability to synaptic weights and delays by specifying a RandomDistribution object as the parameter value:

w = RandomDistribution('gamma', [10, 0.004], rng=NumpyRNG(seed=4242))
syn = StaticSynapse(weight=w, delay=0.5)

It is also possible to specify parameters as a function of the distance (typically in microns, but different scales are possible - see Representing spatial structure and calculating distances) between pre- and post-synaptic neurons:

syn = StaticSynapse(weight=w, delay="0.2 + 0.01*d")

Short-term synaptic plasticity

PyNN currently provides one standard model for short-term synaptic plasticity (facilitation and depression):

depressing_synapse = TsodyksMarkramSynapse(weight=w, delay=0.2, U=0.5,
                                           tau_rec=800.0, tau_facil=0.0)
tau_rec = RandomDistribution('normal', [100.0, 10.0])
facilitating_synapse = TsodyksMarkramSynapse(weight=w, delay=0.5, U=0.04,
                                             tau_rec=tau_rec)

Spike-timing-dependent plasticity

STDP models are specified in a slightly different way than other standard models: an STDP synapse type is constructed from separate weight-dependence and timing-dependence components, e.g.:

stdp = STDPMechanism(
          weight=0.02,  # this is the initial value of the weight
          delay="0.2 + 0.01*d",
          timing_dependence=SpikePairRule(tau_plus=20.0, tau_minus=20.0,
                                          A_plus=0.01, A_minus=0.012),
          weight_dependence=AdditiveWeightDependence(w_min=0, w_max=0.04))

Note that not all simulators will support all possible combinations of synaptic plasticity components.

Connection algorithms

In PyNN, each different algorithm that can be used to determine which pre-synaptic neurons are connected to which post-synaptic neurons (also called a “connection method” or “wiring method”) is encapsulated in a separate class.

Note

for those interested in design patterns, this is an example of the Strategy Pattern

Each such class inherits from a base class, Connector, and must implement a connect() method which takes a Projection object (see below) as its single argument.

PyNN’s library of connection algorithms currently contains the following classes:

All-to-all connections

Each neuron in the pre-synaptic population is connected to every neuron in the post-synaptic population. (In this section, the term “population” should be understood as referring to any of the following: a Population, a PopulationView, or an Assembly object.)

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.:

connector = AllToAllConnector(allow_self_connections=False)  # no autapses

One-to-one connections

Use of the OneToOneConnector requires that the pre- and post-synaptic populations have the same size. The neuron with index i in the pre-synaptic population is then connected to the neuron with index i in the post-synaptic population.

connector = OneToOneConnector()

Trying to connect two populations with different sizes will raise an Exception.

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:

connector = FixedProbabilityConnector(p_connect=0.2)

Connecting neurons with a position-dependent probability

The connection probability can also depend on the positions of the pre- and post-synaptic neurons.

With the DistanceDependentProbabilityConnector, the connection probability depends on the distance between the two neurons.

The constructor requires a string d_expression, which should be a distance expression, as described above for delays, but returning a probability (a value between 0 and 1):

DDPC = DistanceDependentProbabilityConnector
connector = DDPC("exp(-d)")
connector = DDPC("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.

Calculation of distance may be controlled by specifying a Space object, passed to the Projection constructor (see below).

For a more general dependence of connection probability on position, use the IndexBasedProbabilityConnector, which expects a function of the indices, i and j, of the pre- and post-synaptic neurons. The function should return the probability of creating that connection.

Divergent/fan-out connections

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

connector = FixedNumberPostConnector(n=30)

If n is less than the size of the post-synaptic population, there are no multiple connections, i.e., no instances of the same pair of neurons being multiply connected. If n is greater than the size of the pre-synaptic population, all possible single connections are made before starting to add duplicate connections.

The number of post-synaptic neurons n can be fixed, or can be chosen at random from a RandomDistribution object, e.g.:

distr_npost = RandomDistribution(distribution='binomial', n=100, p=0.3)
connector = 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.:

connector = FixedNumberPreConnector(5)
distr_npre = RandomDistribution(distribution='poisson', lambda_=5)
connector = FixedNumberPreConnector(distr_npre)

Creating a small-world network

Todo

Pierre to write this bit?

Using the Connection Set Algebra

The Connection Set Algebra (Djurfeldt, 2012) 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
cset = csa.full - csa.oneToOne
connector = CSAConnector(cset)

csa.full represents all-to-all connections, while csa.oneToOne represents the connection of pre-synaptic neuron i to post-synaptic neuron i. By subtracting the second from the first, the connection rule is “all-to-all, except where the neurons have the same index”. If the pre- and post-synaptic populations are the same population, this is equivalent to AllToAllConnector(allow_self_connections=False).

Todo

explain that weights and delays can either be specified within the connection set or within the synapse type.

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. Optionally, the list can contain synaptic properties such as weights, delays, or the parameters for plasticity rules. Example:

connections = [
  (0, 0, 0.0, 0.1),
  (0, 1, 0.0, 0.1),
  (0, 2, 0.0, 0.1),
  (1, 5, 0.0, 0.1)
]
connector = FromListConnector(connections, column_names=["weight", "delay"])

Any synaptic parameters not given in the list are determined from the synapse type. Parameters given in the list always override the values from the synapse type.

Reading connection patterns to/from a file

Connection patterns can be read in from a text file. The file should contain a header specifying which parameter is in which column, e.g.:

# columns = ["i", "j", "weight", "delay", "U", "tau_rec"]

and then the connection data should be in columns separated by spaces. The connections are read using:

connector = FromFileConnector("connections.txt")

Specifying an explicit connection matrix

The connectivity can be specified as a boolean array, where each row represents the existence of connections from a given pre-synaptic neuron to the post-synaptic neurons. For example:

connections = numpy.array([[0, 1, 1, 0],
                           [1, 1, 0, 1],
                           [0, 0, 1, 0]],
                          dtype=bool)
connector = ArrayConnector(connections)

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:

Projections

A Projection is a container for a set of connections between two populations of neurons, where by population we mean one of:

  • a Population object - a group of neurons all of the same type;

  • a PopulationView object - part of a Population;

  • a Assembly - a heterogeneous group of neurons, which may be of different types.

Creating a Projection in PyNN also creates the connections at the level of the simulator. To create a Projection we must specify:

  • the pre-synaptic population;

  • the post-synaptic population;

  • a connection/wiring method;

  • a synapse type

Optionally, we can also specify:

  • the name of the post-synaptic mechanism (e.g. ‘excitatory’, ‘NMDA’) (if not specified, PyNN picks a default depending on the weight parameter of the synapse type);

  • a label (autogenerated if not specified);

  • a Space object, which determines how distances should be calculated for distance-dependent wiring schemes or parameter values.

Here is a minimal example:

excitatory_connections = Projection(pre, post, AllToAllConnector(),
                                    StaticSynapse(weight=0.123))

and here is a full example:

rng = NumpyRNG(seed=64754)
sparse_connectivity = FixedProbabilityConnector(0.1, rng=rng)
weight_distr = RandomDistribution('normal', [0.01, 1e-3], rng=rng)
facilitating = TsodyksMarkramSynapse(U=0.04, tau_rec=100.0, tau_facil=1000.0,
                                     weight=weight_distr, delay=lambda d: 0.1+d/100.0)
space = Space(axes='xy')
inhibitory_connections = Projection(pre, post,
                                    connector=sparse_connectivity,
                                    synapse_type=facilitating,
                                    receptor_type='inhibitory',
                                    space=space,
                                    label="inhibitory connections")

Note that the attribute receptor_types of all cell type classes contains a list of the possible values of receptor_type for that cell type:

>>> post
Population(10, IF_cond_exp(<parameters>), structure=Line(y=0.0, x0=0.0, z=0.0, dx=1.0), label='population1')
>>> post.celltype
IF_cond_exp(<parameters>)
>>> post.celltype.receptor_types
('excitatory', 'inhibitory')

The space argument is used to specify how to calculate distances, since we have used a distance expression to specify the connection delay, modelling a constant axonal propagation speed.

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

space = Space(axes='xy')

will ignore the z-coordinate when calculating distance. Similarly, the origins of the coordinate systems of the two populations 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.:

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). For more information, see Representing spatial structure and calculating distances.

Accessing weights and delays

The Projection.get() method allows the retrieval of connection attributes, such as weights and delays. 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):

>>> excitatory_connections.get('weight', format='list')[3:7]
[(3, 0, 0.123), (4, 0, 0.123), (5, 0, 0.123), (6, 0, 0.123)]
>>> inhibitory_connections.get('delay', format='array')[:3,:5]
array([[  nan,   nan,   nan,   nan,  0.14],
       [  nan,   nan,   nan,  0.12,  0.13],
       [ 0.12,   nan,   nan,   nan,   nan]])

To suppress the coordinates of the connection in 'list', view, set the with_address option to False:

>>> excitatory_connections.get('weight', format='list', with_address=False)[3:7]
[0.123, 0.123, 0.123, 0.123]

As well as weight and delay, Projection.get() can also retrieve any other parameters of synapse models:

>>> inhibitory_connections.get('U', format='list')[0:4]
[(2, 0, 0.04), (6, 1, 0.04), (8, 1, 0.04), (9, 2, 0.04)]

It is also possible to retrieve the values of multiple attributes at once, as either a list of tuples or a tuple of arrays:

>>> connection_data = inhibitory_connections.get(['weight', 'delay'], format='list')
>>> for connection in connection_data[:5]:
...    src, tgt, w, d = connection
...    print("weight = %.4f  delay = %4.2f" % (w, d))
weight = 0.0094  delay = 0.12
weight = 0.0113  delay = 0.15
weight = 0.0102  delay = 0.17
weight = 0.0097  delay = 0.17
weight = 0.0127  delay = 0.12
>>> weights, delays = inhibitory_connections.get(['weight', 'delay'], format='array')
>>> exists = ~numpy.isnan(weights)
>>> for w, d in zip(weights[exists].flat, delays[exists].flat)[:5]:
...    print("weight = %.4f  delay = %4.2f" % (w, d))
weight = 0.0097  delay = 0.14
weight = 0.0127  delay = 0.12
weight = 0.0097  delay = 0.13
weight = 0.0094  delay = 0.18
weight = 0.0094  delay = 0.12

Note that in this last example we have filtered out the non-existent connections using numpy.isnan().

The Projection.save() method saves connection attributes to disk.

Todo

finish documenting save() method (also decide if it should be write() or save()) need to think about formats. Text, HDF5, …

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

>>> list(inhibitory_connections.connections)[0].weight
0.0094460775218037779
>>> list(inhibitory_connections.connections)[10].weight
0.0086313719119562281

Modifying weights and delays

As noted above, weights, delays and other connection attributes can be specified on creation of a Projection, and this is generally the most efficient time to specify them. It is also possible, however, to modify these attributes after creation, using the set() method.

set() accepts any number of keyword arguments, where the key is the attribute name, and the value is either:

  • a numeric value (all connections will be set to the same value);

  • a RandomDistribution object (each connection will be

    set to a different value, drawn from the distribution);

  • a list or NumPy array of the same length as the number of connections in the Projection;

  • a generator;

  • a string expressing a function of the distance between pre- and post-synaptic neurons.

Todo

clarify whether this is the number of local connections or the total number of connections.

Some examples:

>>> excitatory_connections.set(weight=0.02)
>>> excitatory_connections.set(weight=RandomDistribution('gamma', [1, 0.1]),
...                            delay=0.3)
>>> inhibitory_connections.set(U=numpy.linspace(0.4, 0.6, len(inhibitory_connections)),
...                            tau_rec=500.0,
...                            tau_facil=0.1)

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

>>> for c in list(inhibitory_connections.connections)[:5]:
...   c.weight *= 2

although this is almost always less efficient than using list- or array-based access.