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
orArrayConnector
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 aPopulation
;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.