ANNarchy 5.0.0
  • ANNarchy
  • Installation
  • Tutorial
  • Manual
  • Notebooks
  • Reference

Projections

  • Core objects
    • Overview
    • Equation parser
    • Rate-coded neurons
    • Spiking neurons
    • Populations
    • Rate-coded synapses
    • Spiking synapses
    • Projections
    • Connectors
    • Monitors
    • Network
    • Setting inputs
    • Random distributions
    • Numerical methods
    • Saving and loading
    • Parallel simulations
  • Extensions
    • Hybrid networks
    • Structural plasticity
    • Convolution and pooling
    • Reporting
    • Logging with tensorboard

On this page

  • Creating the projections
  • Instantiating the projections
  • Projection attributes
    • Global attributes
    • Local attributes
    • Functions
  • Connecting population views
  • Specifying delays in synaptic transmission
  • Controlling projections
  • Multiple targets

Projections

Let’s suppose the BCM synapse should used to create a plastic projection between two populations :

BCM = ann.Synapse(
    parameters = dict(
        eta = 0.01,
        tau = 100.,
    ),
    equations = [
        ann.Variable('tau * dtheta/dt + theta = post.r^2', locality='semiglobal'),
        ann.Variable('dw/dt = eta * post.r * (post.r - theta) * pre.r', min=0.0),
    ]
)

and two populations have been created:

net = ann.Network()
pop1 = net.create(10, ann.Neuron(equations="r=sum(exc)"))
pop2 = net.create(10, ann.Neuron(equations="r=sum(exc)"))

Creating the projections

Once the populations are created, one can connect them by creating Projection instances through the Network.connect() method:

proj = net.connect(
    pre = pop1,
    post = pop2,
    target = "exc",
    synapse = BCM
)
  • pre is either the name of the pre-synaptic population or the corresponding Population object.
  • post is either the name of the post-synaptic population or the corresponding Population object.
  • target is the type of the connection.
  • synapse is an optional argument requiring a Synapse instance.

The post-synaptic neuron type must use sum(exc) in the rate-coded case, respectively g_exc in the spiking case, otherwise the projection will be useless.

If the synapse argument is omitted, the default synapse will be used:

  • the default rate-coded synapse defines psp = w * pre.r,
  • the default spiking synapse defines g_target += w.

Instantiating the projections

Creating the Projection objects only defines the information that two populations are connected. The synapses must be explicitely created by applying a connector method on the Projection object.

To this end, ANNarchy already provides a set of predefined connector methods, but the user has also the possibility to define his own (see the following page Connectivity).

The connection pattern should be applied right after the creation of the Projection:

proj.fixed_probability(probability=0.5, weights = 1.0)
<ANNarchy.core.Projection.Projection at 0x7fb20c61b430>

The connector method must be called before the network is compiled.

Projection attributes

Global attributes

The global parameters and variables of a projection (parameters by default, and variables defined with locality='global') can be accessed directly through attributes:

proj.tau
100.0

Semi-global attributes have one value per post-synaptic neuron, so the result is a list:

proj.theta
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

Post-synaptic variables can be modified by passing:

  • a single value, which will be the same for all post-synaptic neurons.
  • a list of values, with the same size as the number of neurons receiving synapses.
  • a list of lists, matching exactly the number of synapses in the projection.

After compilation (and therefore creation of the synapses), you can access how many post-synaptic neurons receive actual synapses with:

proj.size
10

The list of ranks of the post-synaptic neurons receiving synapses is obtained with:

proj.post_ranks
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Local attributes

Beware: As projections are only instantiated after the call to compile(), local attributes of a Projection are only available then. Trying to access them before compilation will lead to an error!

At the projection level

Local attributes can also be accessed globally through attributes. It will return a list of lists containing the synapse-specific values.

proj.w
[[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
 [1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
 [1.0, 1.0, 1.0, 1.0, 1.0],
 [1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
 [1.0, 1.0, 1.0, 1.0],
 [1.0, 1.0, 1.0, 1.0],
 [1.0, 1.0, 1.0, 1.0],
 [1.0, 1.0, 1.0, 1.0, 1.0],
 [1.0, 1.0, 1.0, 1.0, 1.0],
 [1.0, 1.0, 1.0, 1.0]]

The first index represents the post-synaptic neurons. It has the same length as proj.post_ranks. Beware that if some post-synaptic neurons do not receive any connection, this index will not correspond to the ranks of the post-synaptic population.

The second index represents the pre-synaptic neurons. The list of pre-synaptic ranks for each post-synaptic neuron is obtained with:

proj.pre_ranks
[[0, 1, 2, 3, 4, 7, 8, 9],
 [2, 3, 5, 6, 7, 8],
 [0, 1, 2, 4, 6],
 [1, 4, 5, 6, 7, 9],
 [2, 5, 8, 9],
 [3, 6, 8, 9],
 [1, 5, 6, 8],
 [0, 1, 3, 4, 6],
 [1, 3, 4, 7, 9],
 [0, 4, 5, 7]]

proj.w and proj.pre_ranks have the same number of elements. In the general case, the connectivity matrix is not dense (different number of incoming synapses for each post-synaptic neuron), so projection attributes cannot be casted to 2D numpy arrays.

The connectivity matrix with weights w can be visualized as a 2D array using Projection.connectivity_matrix(), replacing non-existing synapse with 0.0 (or any other value of your choice).

proj.connectivity_matrix(fill=0.0)
array([[1., 1., 1., 1., 1., 0., 0., 1., 1., 1.],
       [0., 0., 1., 1., 0., 1., 1., 1., 1., 0.],
       [1., 1., 1., 0., 1., 0., 1., 0., 0., 0.],
       [0., 1., 0., 0., 1., 1., 1., 1., 0., 1.],
       [0., 0., 1., 0., 0., 1., 0., 0., 1., 1.],
       [0., 0., 0., 1., 0., 0., 1., 0., 1., 1.],
       [0., 1., 0., 0., 0., 1., 1., 0., 1., 0.],
       [1., 1., 0., 1., 1., 0., 1., 0., 0., 0.],
       [0., 1., 0., 1., 1., 0., 0., 1., 0., 1.],
       [1., 0., 0., 0., 1., 1., 0., 1., 0., 0.]])
Warning

Modifying these lists of lists will not change the underlying connectivity, they are read-only. Only use them for analysis.

At the post-synaptic level

To minimize memory access, the local parameters and variables of a projection can be accessed through the Dendrite object, which gathers for a single post-synaptic neuron all synapses belonging to the projection.

Each dendrite stores the parameters and variables of the corresponding synapses as attributes, as populations do for neurons. You can loop over all post-synaptic neurons receiving synapses with the dendrites iterator:

for dendrite in proj.dendrites:
    print(dendrite.pre_ranks, dendrite.size)
[0, 1, 2, 3, 4, 7, 8, 9] 8
[2, 3, 5, 6, 7, 8] 6
[0, 1, 2, 4, 6] 5
[1, 4, 5, 6, 7, 9] 6
[2, 5, 8, 9] 4
[3, 6, 8, 9] 4
[1, 5, 6, 8] 4
[0, 1, 3, 4, 6] 5
[1, 3, 4, 7, 9] 5
[0, 4, 5, 7] 4

dendrite.pre_ranks returns a list of pre-synaptic neuron ranks. dendrite.size returns the number of synapses for the considered post-synaptic neuron. Global parameters/variables return a single value (dendrite.tau) and local ones return a list (dendrite.w).

You can also access the dendrites individually, either by specifying the rank or the coordinates of the post-synaptic neuron:

dendrite = proj.dendrite(3)
print(dendrite.w)
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
Warning

You should make sure that the dendrite actually exists before accessing it through its rank, because it is otherwise a None object.

Functions

If you have defined a function inside a Synapse definition:

BCM = ann.Synapse(
    parameters = dict(
        eta = 0.01,
        tau = 100.,
    ),
    equations = [
        ann.Variable('tau * dtheta/dt + theta = post.r^2', 'semiglobal'),
        ann.Variable('dw/dt = eta * BCMRule(pre.r, post.r, theta)', min=0.0),
    ]
    functions = """
        BCMRule(pre, post, theta) = post * (post - theta) * pre
    """
)

you can use this function in Python as if it were a method of the corresponding object:

proj = net.connect(pop1, pop2, 'exc', BCM)
proj.all_to_all(weights=1.0)

pre = np.linspace(0., 1., 100)
post = np.linspace(0., 1., 100)
theta = 0.01 * np.ones(100)

weight_change = proj.BCMRule(pre, post, theta)

You can pass either a list or a 1D Numpy array to each argument (not a single value, nor a multidimensional array!).

The size of the arrays passed for each argument is arbitrary (it must not match the projection’s size) but you have to make sure that they all have the same size. Errors are not catched, so be careful.

Connecting population views

Projections are usually understood as a connectivity pattern between two populations. Complex connectivity patterns have to specifically designed (see Connectivity). In some cases, it can be much simpler to connect subsets of neurons directly, using built-in connector methods. To this end, the Projection object also accepts PopulationView objects for the pre and post arguments.

Let’s suppose we want to connect the (8, 8) populations pop1 and pop2 in a all-to-all manner, but only for the (4,4) neurons in the center of these populations. The first step is to create the PopulationView objects using the slice operator:

pop1_center = pop1[2:7, 2:7]
pop2_center = pop2[2:7, 2:7]

They can then be simply used to create a projection:

proj = net.connect(
    pre = pop1_center,
    post = pop2_center,
    target = "exc",
    synapse = BCM
)

proj.all_to_all(weights=1.0)

Each neuron of pop2_center will receive synapses from all neurons of pop1_center, and only them. Neurons of pop2 which are not in pop2_center will not receive any synapse.

Warning

If you define your own connector method and want to use PopulationViews, you will need to iterate over the ranks attribute of the PopulationView object.

Specifying delays in synaptic transmission

By default, synaptic transmission is considered to be instantaneous (or more precisely, it takes one simulation step (dt) for a newly computed firing rate to be taken into account by post-synaptic neurons).

In order to take longer propagation times into account in the transmission of information between two populations, one has the possibility to define synaptic delays for a projection. All the built-in connector methods take an argument delays (default=dt), which can be a float (in milliseconds) or a random number generator.

proj.all_to_all(weights = 1.0, delays = 10.0)
proj.all_to_all(weights = 1.0, delays = Uniform(1.0, 10.0))

If the delay is not a multiple of the simulation time step (dt = 1.0 by default), it will be rounded to the closest multiple. The same is true for the values returned by a random number generator.

Note: Per design, the minimal possible delay is equal to dt: values smaller than dt will be replaced by dt. Negative values do not make any sense and are ignored.

Warning

Non-uniform delays are not available on CUDA.

Controlling projections

Synaptic transmission, update and plasticity

It is possible to selectively control synaptic transmission and plasticity at the projection level. The boolean flags transmission, update and plasticity can be set for that purpose:

proj.transmission = False
proj.update = False
proj.plasticity = False
  • If transmission is False, the projection is totally shut down: it does not transmit any information to the post-synaptic population (the corresponding weighted sums or conductances are constantly 0) and all synaptic variables are frozen to their current value (including the synaptic weights w).
  • If update is False, synaptic transmission occurs normally, but the synaptic variables are not updated. For spiking synapses, this includes traces when they are computed at each step, but not when they are integrated in an event-driven manner (flag event-driven). Beware: continous synaptic transmission as in NMDA synapses will not work in this mode, as internal variables are not updated.
  • If only plasticity is False, synaptic transmission and synaptic variable updates occur normally, but changes to the synaptic weight w are ignored.

Disabling learning

Alternatively, one can use the enable_learning() and disable_learning() methods of Projection. The effect of disable_learning() depends on the type of the projection:

  • for rate-coded projections, disable_learning() is equivalent to update=False: no synaptic variables is updated.
  • for spiking projections, it is equivalent to plasticity=False: only the weights are blocked.

The reason of this difference is to allow continuous synaptic transmission and computation of traces. Calling enable_learning() without arguments resumes the default learning behaviour.

Periodic learning

enable_learning() also accepts two arguments period and offset. period defines the interval in ms between two evaluations of the synaptic variables. This can be useful when learning should only occur once at the end of a trial. It is recommended not to use ODEs in the equations in this case, as they are numerized according to a fixed time step. offset defines the time inside the period at which the evaluation should occur. By default, it is 0, so the variable updates will occur at the next step, then after period ms, and so on. Setting it to -1 will shift the update at the end of the period.

Note that spiking synapses using online evaluation will not be affected by these parameters, as they are event-driven.

Multiple targets

For spiking neurons, it may be desirable that a single synapses activates different currents (or conductances) in the post-synaptic neuron. One example are AMPA/NMDA synapses, where a single spike generates a “classical” AMPA current, plus a voltage-gated slower NMDA current. The following conductance-based Izhikevich is an example:

RSNeuron = ann.Neuron(
    parameters = dict(
        a = 0.02,
        b = 0.2,
        c = -65.,
        d = 8.,
        tau_ampa = 5.,
        tau_nmda = 150.,
        vrev = 0.0,
     ) ,
    equations = [
        'I = g_ampa * (vrev - v) + g_nmda * nmda(v, -80.0, 60.0) * (vrev -v)',
        'dv/dt = 0.04 * v^2 + 5.0 * v + 140.0 - u + I',
        'du/dt = a * (b*v - u)',
        'tau_ampa * dg_ampa/dt = -g_ampa',
        'tau_nmda * dg_nmda/dt = -g_nmda',
    ],
    spike = """
        v >= 30.
    """,
    reset = """
        v = c
        u += d
    """,
    functions = """
        nmda(v, t, s) = ((v-t)/(s))^2 / (1.0 + ((v-t)/(s))^2)
    """
)

However, g_ampa and g_nmda collect by default spikes from different projections, so the weights will not be shared between the "ampa" projection and the "nmda" one. It is therefore possible to specify a list of targets when building a projection, meaning that a single pre-synaptic spike will increase both g_ampa and g_nmda from the same weight:

proj = net.connect(pop1, pop2, ['ampa', 'nmda'], STDP)

An example is provided in /notebooks/Ramp.ipynb.

Warning

Multiple targets are not available on CUDA yet.

Spiking synapses
Connectors
 

Copyright Julien Vitay, Helge Ülo Dinkelbach, Fred Hamker