Connectivity
There are basically four methods to instantiate projections:
- By using a built-in connector method.
- By using a saved projection.
- By loading dense or sparse matrices.
- By defining a custom connector method.
Available connector methods
For further detailed information about these connectors, please refer to the library reference Projections.
connect_all_to_all
All neurons of the post-synaptic population form connections with all neurons of the pre-synaptic population (dense connectivity). Self-connections are avoided by default, but the parameter allow_self_connections
can be set to True
:
=1.0, delays=2.0, allow_self_connections=False) proj.connect_all_to_all(weights
The weights
and delays
arguments accept both single float values (all synapses will take this initial value), as well as random objects allowing to randomly select the initial values for different synapses:
=Uniform(0.0, 0.5)) proj.connect_all_to_all(weights
connect_one_to_one
A neuron of the post-synaptic population forms a connection with only one neuron of the pre-synaptic population. The order depends on the ranks: neuron 0 is connected with neuron 0 and so on. It is advised that the pre- and post-populations have the same size/geometry, especially when using population views.
= Population((20, 20), Neuron(parameters="r=0.0"))
pop1 = Population((10, 10), Neuron(equations="r=sum(exc)"))
pop2
= Projection(pop1[5:15, 5:15], pop2, 'exc')
proj =1.0) proj.connect_one_to_one(weights
Weights and delays also accept random distributions.
Below is a graphical representation of the difference between all_to_all and one_to_one:
connect_gaussian
A neuron of the post-synaptic population forms a connection with a limited region of the pre-synaptic population, centered around the neuron with the same normalized position. Weight values are initialized using a Gaussian function, with a maximal value amp
for the neuron of same position and decreasing with distance (standard deviation sigma
):
w(x, y) = A \, \exp(-\dfrac{1}{2}\dfrac{(x-x_c)^2+(y-y_c)^2}{\sigma^2})
where (x, y) is the position of the pre-synaptic neuron (normalized to [0, 1]^d) and (x_c, y_c) is the position of the post-synaptic neuron (normalized to [0, 1]^d). A = amp, sigma = \sigma.
In order to void creating useless synapses, the parameter limit
can be set to restrict the creation of synapses to the cases where the value of the weight would be superior to limit*abs(amp)
. Default is 0.01 (1%).
Self-connections are avoided by default (parameter allow_self_connections
).
The two populations must have the same number of dimensions, but the number of neurons can vary as the positions of each neuron are normalized in [0, 1]^d:
=1.0, sigma=0.2, limit=0.001) proj.connect_gaussian( amp
connect_dog
The same as connect_gaussian, except weight values are computed using a Difference-of-Gaussians (DoG), usually positive in the center, negative a bit further away and small at long distances.
\begin{aligned} w(x, y) &= A^+ \, \exp(-\frac{1}{2}\frac{(x-x_c)^2+(y-y_c)^2}{\sigma_+^2}) \\ &- A^- \, \exp(-\frac{1}{2}\frac{(x-x_c)^2+(y-y_c)^2}{\sigma_-^2}) \\ \end{aligned}
Weights smaller than limit * abs(amp_pos - amp_neg)
are not created and self-connections are avoided by default (parameter allow_self_connections
):
proj.connect_dog(=1.0, sigma_pos=0.2,
amp_pos=0.3, sigma_neg=0.7,
amp_neg=0.001
limit )
The following figure shows the example of a neuron of coordinates (10, 10) in the post-synaptic population, which is connected through the gaussian (left) and dog (right) projections to a population of geometry 30*30. The X and Y axis denote the coordinates of the pre-synaptic neurons, while the Z axis is the weight value.
connect_fixed_number_pre
Each neuron in the post-synaptic population receives connections from a fixed number of neurons of the pre-synaptic population chosen randomly. It may happen that two post-synaptic neurons are connected to the same pre-synaptic neuron and that some pre-synaptic neurons are connected to nothing:
= 20, weights=1.0) proj.connect_fixed_number_pre(number
weights
and delays
can also take a random object.
connect_fixed_number_post
Each neuron in the pre-synaptic population sends a connection to a fixed number of neurons of the post-synaptic population chosen randomly. It may happen that two pre-synaptic neurons are connected to the same post-synaptic neuron and that some post-synaptic neurons receive no connection at all:
= 20, weights=1.0) proj.connect_fixed_number_post(number
The following figure shows the fixed_number_pre (left) and fixed_number_post projections between two populations of 4 neurons, with number=2
. In fixed_number_pre, each post-synaptic neuron receives exactly 2 connections, while in fixed_number_post, each pre-synaptic neuron send exactly two connections:
connect_fixed_probability
For each post-synaptic neuron, there is a fixed probability that it forms a connection with a neuron of the pre-synaptic population. It is basically a all_to_all projection, except some synapses are not created, making the projection sparser:
= 0.2, weights=1.0) proj.connect_fixed_probability(probability
If a single value is used for the weights
argument of connect_all_to_all
, connect_one_to_one
, connect_fixed_probability
, connect_fixed_number_pre
and connect_fixed_number_post
, and the default synapse is used (no synaptic plasticity), ANNarchy will generate a single weight value for all the synapses of the projection, not one per synapse.
This allows to save a lot of memory and improve performance. However, if you wish to manually change the weights of some of the synapses after the creation, you need to force the creation of one value per synapse by setting force_multiple_weights=True
in the call to the connector.
Saved connectivity
It is also possible to build a connection pattern using data saved during a precedent simulation. This is useful when:
- pre-learning is done in another context;
- a connector method for static synapses is particularly slow (e.g. DoG), but loading the result from a file is faster.
The connectivity of a projection can be saved (after compile()
) using:
='proj.npz') proj.save_connectivity(filename
The filename can used relative or absolute paths. The data is saved in a binary format:
- Compressed Numpy format when the filename ends with
.npz
. - Compressed binary file format when the filename ends with
.gz
. - Binary file format otherwise.
It can then be used to instantiate another projection:
='proj.npz') proj.connect_from_file(filename
Only the connectivity (which neurons are connected), the weights and delays are loaded. Other synaptic variables are left untouched. The pre- and post-synaptic population must have the same size during saving and loading.
From connectivity matrices
One can also create connections using Numpy dense matrices or Scipy sparse matrices.
connect_from_matrix
This method accepts a Numpy array to define the weights of the projection (and optionally the delays). By default, the matrix should have the size (post.size, pre.size)
, so that the first index represents a post-synaptic neuron and the second the pre-synaptic neurons. If your matrix is defined in the reversed order, you can either transpose it or set the pre_post
argument to True
.
This method is useful for dense connectivity matrices (all-to-all). If you do not want to create some synapses, the weight value should be set to None
.
The following code creates a synfire chain inside a population of 100 neurons:
= 100
N = ann.Projection(pop, pop, 'exc')
proj
# Initialize an empty connectivity matrix
= np.array([[None]*N]*N)
w
# Connect each post-synaptic neuron to its predecessor
for i in range(N):
-1)%N] = 1.0
w[i, (i
# Create the connections
proj.connect_from_matrix(w)
Connectivity matrices can not work with multi-dimensional coordinates, only ranks are used. Population views can be used in the projection, but the connection matrix must have the corresponding size:
= ann.Projection(pop[10:20], pop[50:60], 'exc')
proj
# Create the connectivity matrix
= np.ones((10, 10))
w
# Create the connections
proj.connect_from_matrix(w)
connect_from_sparse
For sparse connection matrices, the Numpy array format may have a huge memory overhead if most of its values are None. It is possible to use Scipy sparse matrices in that case. The previous synfire chain example becomes:
from scipy.sparse import lil_matrix
= ann.Projection(pop, pop, 'exc')
proj
= lil_matrix((N, N))
w for i in range(N):
+1)%N] = 1.0
w[i, (i
proj.connect_from_sparse(w)
Contrary to connect_from_matrix()
, the first index of the sparse matrix represents the pre-synaptic neurons, not the post-synaptic ones. This is for compatibility with other neural simulators.
connect_from_sparse()
accepts lil_matrix
, csr_matrix
and csc_matrix
objects, although lil_matrix
should be preferred for its simplicity of element access.
User-defined patterns
This section describes the creation of user-specific connection patterns in ANNarchy, if the available patterns are not enough. A connection pattern is simply implemented as a method returning a LILConnectivity
(list-of-list) object containing all the necessary information to create the synapses.
A connector method must take on the first position the pre-synaptic population (or a subset of it) and on the second one the post-synaptic population. Other arguments are free, but should be passed when creating the projection.
<other arguments>) probabilistic_pattern(pre, post,
As an example, we will recreate the fixed_probability connector method, building synapses with a given probability. For this new pattern we need a weight value (common for all synapses) and a probability value as additional arguments. We consider that no delay is introduced in the synaptic transmission.
def probabilistic_pattern(pre, post, weight, probability):
= ann.LILConnectivity()
synapses
... pattern code comes here ...
return synapses
fixed_probability in Python
The connector method needs to return a LILConnectivity
object storing the connectivity. For each post-synaptic neuron receiving synapses, a list of pre-synaptic ranks, weight values and delays must be added to the structure. If you use 2D or 3D populations you need to transform the coordinates into ranks with the rank_from_coordinates
function.
import random
import ANNarchy as ann
def probabilistic_pattern(pre, post, weight, probability):
# Create a LIL structure for the connectivity matrix
= ann.LILConnectivity()
synapses # For all neurons in the post-synaptic population
for post_rank in range(post.size):
# Decide which pre-synaptic neurons should form synapses
= []
ranks for pre_rank in range(pre.size):
if random.random() < probability:
ranks.append(pre_rank)# Create weights and delays arrays of the same size
= [weight for i in range(len(ranks)) ]
values = [0 for i in range(len(ranks)) ]
delays # Add this information to the LIL matrix
synapses.add(post_rank, ranks, values, delays)
return synapses
The first for - loop iterates over all post-synaptic neurons in the projection. The inner for loop decides for each of these neurons if a synapse with a pre-synaptic neuron should be created, based on the value probability
provided as argument to the function.
The lists values
and delays
are then created with the same size as ranks
(important!), and filled with the desired value. All this information is then fed into the LIL matrix using the add(post_rank, ranks, values, delays)
method.
Building such connectivity matrices in Python can be extremely slow, as Python is not made for tight nested loops. If the construction of your network lasts too long, you should definitely write this function in Cython.
The add()
should be only called once per post-synaptic neuron! If not, ANNarchy will have to reorder its internal representations and this will be really slow.
Usage of the pattern
To use the pattern within a projection you provide the pattern method to the connect_with_func
method of Projection
= ann.Projection(
proj = pop1,
pre = pop2,
post = 'inh'
target
)
proj.connect_with_func(=probabilistic_pattern,
method=1.0,
weight=0.3
probability )
method
is the method you just wrote. Extra arguments (other than pre
and post
) should be passed with the same name.
fixed_probability in Cython
For this example, we will create a Cython file CustomPatterns.pyx
in the same directory as the script. Its content should be relatively similar to the Python version, except some type definitions:
# distutils: language = c++
import random
import ANNarchy
as Connector
cimport ANNarchy.cython_ext.Connector
def probabilistic_pattern(pre, post, weight, probability):
# Typedefs
cdef Connector.LILConnectivity synapsesint post_rank, pre_rank
cdef list ranks, values, delays
cdef
# Create a LILConnectivity structure for the connectivity matrix
= Connector.LILConnectivity()
synapses # For all neurons in the post-synaptic population
for post_rank in range(post.size):
# Decide which pre-synaptic neurons should form synapses
= []
ranks for pre_rank in range(pre.size):
if random.random() < probability:
ranks.append(pre_rank)# Create weights and delays arrays of the same size
= [weight for i in range(len(ranks)) ]
values = [0 for i in range(len(ranks)) ]
delays # Add this information to the LILConnectivity matrix
synapses.add(post_rank, ranks, values, delays)
return synapses
The only differences with the Python code are:
- The module
Connector
where theLILConnectivity
connection matrix class is defined should be cimported with:
as Connector cimport ANNarchy.cython_ext.Connector
- Data structures should be declared with
cdef
at the beginning of the method:
# Typedefs
cdef Connector.LILConnectivity synapsesint post_rank, pre_rank
cdef list ranks, values, delays cdef
To allow Cython to compile this file, we also need to provide with a kind of "Makefile" specifying that the code should be generated in C++, not C. This file should have the same name as the Cython file but end with .pyxbld
, here : CustomPatterns.pyxbld
.
from distutils.extension import Extension
import ANNarchy
def make_ext(modname, pyxfilename):
return Extension(name=modname,
=[pyxfilename],
sources= ANNarchy.include_path(),
include_dirs =['-std=c++11'],
extra_compile_args="c++") language
This .pyxbld
is generic, you don’t need to modify anything, except its name.
Now you can import the method probabilistic_pattern()
into your Python code using the pyximport
module of Cython and build the Projection normally:
import pyximport; pyximport.install()
from CustomPatterns import probabilistic_pattern
=probabilistic_pattern, weight=1.0, probability=0.3) proj.connect_with_func(method
Writing the connector in Cython can bring speedups up to 100x compared to Python if the projection has a lot of synapses.