Table of Contents
Implementing multiple synaptic states (the old way)
Note, that starting with Auryn v0.7 there is a new (and better way) of dealing with multiple synaptic state variables.
Aims
Our aim is to introduce a meta-variable lpw
which characterizes synaptic strength as it is transmitted. It is a low-pass filtered version of the plastic weight w
. Doing so will require to store both variables for each synapse. Therefore, each synapse possesses two states that it will need to keep track of. Currently, Auryn does not provide matrix classes which implement this directly. Therefore, we need do things manually.
Walk-through
To achieve our aim we will base this tutorial on the TripletConnection which implements the minimal triplet model by Pfister and Gerstner (2006). In the following we will address these steps:
- We will copy and rename
TripletConnection
into a new class calledLPTripletConnection
- We will then insert a second SimpleMatrix matrix container into the class which will hold our meta-variable
lpw
. - We will then modify the propagate function to propagate
lpw
as the synaptic weight to postsynaptic neurons. - Finally we will implement an
evolve
function for the connection in which we implement the low pass filtering ofw
to yieldlpw
.
Copying TripletSTDPConnection source files
To prepare our new class we start by navigating to Auryn's src
directory. And then copying TripletConnection.h
and TripletConnection.cpp
to LPTripletConnection.h
and LPTripletConnection.cpp
respectively. Note, that you can name the class differently, but you will have to stick with then new name you choose throughout this walk-through. Here, I simply put LP for low-pass, but a shorter or different name might seem more appropriate for you.
zenke@cashew:~/auryn/src$ cp TripletConnection.h LPTripletConnection.h zenke@cashew:~/auryn/src$ cp TripletConnection.cpp LPTripletConnection.cpp
Once the files are copied, open them in your preferred browser and replace all occurrences of TripletConnection
with LPTripletConnection
. Make sure to also replace the include guards in the header file which are in all caps TRIPLETCONNECTION_H_
becomes LPTRIPLETCONNECTION_H_
. Congrats! At this point it should be possible to build Auryn with the new object. However, so far we have not really implemented any new functionality. The new class will therefore behave exactly the same way as the original TripletConnection.
Now is generally a good time to start updating the Doxygen string in the header file just above the keyword class
. Add a short description of what you are planning to do with this connection, otherwise this is only forgotten later and you will have created ambiguous documentation which is only going to confuse you later.
Adding a second SimpleMatrix container
To store synaptic weights Auryn uses the class SimpleMatrix. The simple denotes that it can hold only a single value (a ComplexMatrix is planned for one of the downstream revisions of Auryn, but not implemented yet). Since we only need a second value to store the low pass filtered version of the weight (the one we called lpw
so far), we will just go with not so need, but fully functional solution of using two SimpleMatrix instances.
First add the following line to the private variable declarations in the newly created header (.h) file
private: ForwardMatrix * lpw;
Then open the .cpp file and find the function init()
. At the end of this function add the following code which will instantiate our new lpw_matrix
lpw = new ForwardMatrix ( w );
This tells Auryn to create a matrix and clone all its properties (such as dimensions, sparseness, etc) from w
which is the default name of the weight matrix in all sparse connections (i.e. also the descendants of SparseConnection). As a next step find the function free()
in the .cpp file and add the line
delete lpw;
You have now created and allocated an object which will house your variable lpw
for each synapse. However, so far this object is not referenced from anywhere, which we will change in the next step. For now it is a good idea to test if LPTripletConnection
still compiles without a problem.
Modify STDP/Plasticity to act on new meta-variable
Since in TripletConnection the weight w
is directly affected by STDP and it also represents the synaptic weight propagated downstream in the case of a presynaptic action potential, we have to break this symmetry of implementation now. To do so, find the propagate_forward()
function in the .cpp file which is responsible for propagating spikes and for handling plasticity events occurring with a presynaptic spike. Find the lines
AurynWeight value = fwd_data[c-fwd_ind]; transmit( *c , value );
What happens here is the following: c
is a pointer to the position of postsynaptic index of the synaptic weight in the sparse matrix object. fwd_data
is a predefined pointer to the beginning of the dense vector storing all nonzero weights of the sparse matrix (it is initialized in init_shortcuts()
). Finally transmit( *c, value)
takes this value and sends it to the downstream neuron with the NeuronID stored in *c
. This way a spike gets propagated to all the postsynaptic partners of a neuron. However, currently fwd_data
points to our w
matrix object, but we would like to propagate a low pass filtered version of this which will ultimately be stored in lpw
. To do this we only need to change these two lines to
AurynWeight value = lpw->get_data_begin()[c-fwd_ind]; transmit( *c , value );
This change will now propagate weights stored in lpw
, but so far these weights are not plastic, which we will change in the next step. For now it is a good idea to save the changes and try compiling the new class again to see if everything runs without error.
Implementing the evolve function to do the low-pass filtering
So far the values stored in lpw
do not change over time. We are now going to change that by implementing the method evolve()
which will do the actual low-pass filtering of w
and store the results in lpw
. Since this function is called in every simulation time step and it affects all the weights (~O(N^2)) is is a good idea to wrap slow processes in a construct that only runs them every so many time steps. To do this we start by adding the following lines to the header .h file.
AurynFloat tau_lp; AurynFloat delta_lp; AurynTime timestep_lp;
just below the definition of lpw
.
Then at the end of init()
in the .cpp file we initialize these values with the following code
tau_lp = 120; timestep_lp = 1e-3*tau_lp/dt; delta_lp = 1.0*timestep_lp/tau_lp*dt;
Here tau_lp
corresponds to the filter time constant of two minutes.
Now find the function evolve()
in the .cpp file (which should be empty) and add the following code
if ( sys->get_clock()%timestep_lp == 0 && stdp_active ) { for (AurynLong i = 0 ; i < lpw->get_nonzero() ; ++i ) { AurynWeight * wval = w->get_data_begin()+i; AurynWeight * lpwval = lpw->get_data_begin()+i; AurynFloat dw = ( *wval - *lpwval ) * delta_lp; *lpwval += dw; } }
which now implements the low-pass filter. Importantly, the update off all lpw
values is only run when the condition sys→get_clock()%timestep_lp == 0
is fulfilled (every timestep_lp time steps). Note, the important computation happens in the line AurynFloat dw = ( *wval - *lpwval ) * delta_lp;
which could be modified by additional functions to achieve non-linear filtering effects such as could be caused by finite availability of for instance AMPA receptors or scaffolding proteins necessary to implement plastic changes.