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

constructing a list or array of connections and using the

`FromListConnector`

or`ArrayConnector`

class;using the Connection Set Algebra and the

`CSAConnector`

class;writing your own

`Connector`

class - see the Developers’ guide for guidance on this.

## 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 beset 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.