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.
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.
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.
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)
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).
It is also possible to add variability to synaptic weights and delays by
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)
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.
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.
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
connect() method which takes a
(see below) as its single argument.
PyNN’s library of connection algorithms currently contains the following classes:
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
PopulationView, or an
AllToAllConnector constructor has one
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
connector = AllToAllConnector(allow_self_connections=False) # no autapses
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
connector = OneToOneConnector()
Trying to connect two populations with different sizes will raise an Exception.
Connecting neurons with a fixed probability¶
FixedProbabilityConnector method, each possible connection
between all pre-synaptic neurons and all post-synaptic neurons is created with
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.
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
False are automatically converted to
Calculation of distance may be controlled by specifying a
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,
j, of the pre- and post-synaptic neurons. The function should
return the probability of creating that connection.
FixedNumberPostConnector connects each pre-synaptic neuron to
n post-synaptic neurons chosen at random:
connector = FixedNumberPostConnector(n=30)
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
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)
connector = FixedNumberPreConnector(5) distr_npre = RandomDistribution(distribution='poisson', lambda_=5) connector = FixedNumberPreConnector(distr_npre)
Creating a small-world network¶
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
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
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
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:
Projection is a container for a set of connections between two
populations of neurons, where by population we mean one of:
Populationobject - a group of neurons all of the same type;
PopulationViewobject - part of a
Assembly- a heterogeneous group of neurons, which may be of different types.
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’) (by default, this is ‘excitatory’);
- a label (autogenerated if not specified);
Spaceobject, 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')
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
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¶
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,
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
>>> 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
Projection.save() method saves connection attributes to disk.
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).weight 0.0094460775218037779 >>> list(inhibitory_connections.connections).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() 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);
RandomDistributionobject (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
a string expressing a function of the distance between pre- and post-synaptic neurons.
clarify whether this is the number of local connections or the total number of connections.
>>> 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
>>> 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.