GeNN  3.3.0
GPU enhanced Neuronal Networks (GeNN)
Tutorial 2

In this tutorial we will learn to add synapsePopulations to connect neurons in neuron groups to each other with synaptic models. As an example we will connect the ten Hodgkin-Huxley neurons from tutorial 1 in a ring of excitatory synapses.

First, copy the files from Tutorial 1 into a new directory and rename the tenHHModel.cc to tenHHRingModel.cc and tenHHSimulation.cc to tenHHRingSimulation.cc, e.g. on Linux or Mac:

>> cp -r tenHH_project tenHHRing_project
>> cd tenHHRing_project
>> mv tenHHModel.cc tenHHRingModel.cc
>> mv tenHHSimulation.cc tenHHRingSimulation.cc

Finally, to reduce confusion we should rename the model itself. Open tenHHRingModel.cc, change the model name inside,

model.setName("tenHHRing");

Defining the Detailed Synaptic Connections

We want to connect our ten neurons into a ring where each neuron connects to its neighbours. In order to initialise this connectivity we need to add a sparse connectivity initialisation snippet at the top of tenHHRingModel.cc:

{
public:
DECLARE_SNIPPET(Ring, 0);
"$(addSynapse, ($(id_pre) + 1) % $(num_post));\n"
"$(endRow);\n");
SET_CALC_MAX_ROW_LENGTH_FUNC([](unsigned int numPre, unsigned int numPost, const std::vector<double> &pars){ return 1;});
};

The SET_ROW_BUILD_CODE code string will be called to generate each row of the synaptic matrix (connections coming from a single presynaptic neuron) and, in this case, each row consists of a single synapses from the presynaptic neuron $(id_pre) to $(id_pre) + 1 (the modulus operator is used to ensure that the final connection between neuron 9 and 0 is made correctly). In order to allow GeNN to better optimise the generated code we also provide a function that returns the maximum row length. In this case each row always contains only one synapse but, when more complex connectivity is used, the number of neurons in the pre and postsynaptic population as well as any parameters used to configure the snippet can be accessed from this function.

Note
When defining GeNN code strings, the $(VariableName) syntax is used to refer to variables provided by GeNN and the $(FunctionName, Parameter1,...) syntax is used to call functions provided by GeNN.

Because we want to use the GPU to run this initialisation code we, once again, need to override some options:

The defaultSparseConnectivityMode option controls sparse connectivity will be initialised. The VarMode::LOC_DEVICE_INIT_DEVICE setting means that initialisation will be performed on the GPU and no host memory is allocated to store a copy of the connectivity. This setting should generally be the default for new models, but section Sparse connectivity initialisation modes outlines the full range of options and shows how you can control this at for each connection.

Adding Synaptic connections

Now we need additional initial values and parameters for the synapse and post-synaptic models. We will use the standard WeightUpdateModels::StaticPulse weight update model and PostsynapticModels::ExpCond post-synaptic model. They need the following initial variables and parameters:

WeightUpdateModels::StaticPulse::VarValues s_ini(
-0.2); // 0 - g: the synaptic conductance value
1.0, // 0 - tau_S: decay time constant for S [ms]
-80.0); // 1 - Erev: Reversal potential
Note
the WeightUpdateModels::StaticPulse weight update model has no parameters and the PostsynapticModels::ExpCond post-synaptic model has no state variables.

We can then add a synapse population at the end of the modelDefinition(...) function,

"Pop1", "Pop1",
{}, s_ini,
ps_p, {},
initConnectivity<Ring>());

The addSynapsePopulation parameters are

  • WeightUpdateModel: template parameter specifying the type of weight update model (derived from WeightUpdateModels::Base).
  • PostsynapticModel: template parameter specifying the type of postsynaptic model (derived from PostsynapticModels::Base).
  • name string containing unique name of synapse population.
  • mtype how the synaptic matrix associated with this synapse population should be represented. Here SynapseMatrixType::RAGGED_GLOBALG means that there will be sparse connectivity and each connection will have the same weight (-0.2 as specified previously).
  • delayStep integer specifying number of timesteps of propagation delay that spikes travelling through this synapses population should incur (or NO_DELAY for none)
  • src string specifying name of presynaptic (source) population
  • trg string specifying name of postsynaptic (target) population
  • weightParamValues parameters for weight update model wrapped in WeightUpdateModel::ParamValues object.
  • weightVarInitialisers initial values or initialisation snippets for the weight update model's state variables wrapped in a WeightUpdateModel::VarValues object.
  • postsynapticParamValues parameters for postsynaptic model wrapped in PostsynapticModel::ParamValues object.
  • postsynapticVarInitialisers initial values or initialisation snippets for the postsynaptic model wrapped in PostsynapticModel::VarValues object.
  • connectivityInitialiser snippet and any paramaters (in this case there are none) used to initialise the synapse population's sparse connectivity.

Adding the addSynapsePopulation command to the model definition informs GeNN that there will be synapses between the named neuron populations, here between population Pop1 and itself. As always, the modelDefinition function ends on

model.finalize();

At this point our model definition file tenHHRingModel.cc should look like this

// Model definition file tenHHRing.cc
#include "modelSpec.h"
{
public:
DECLARE_SNIPPET(Ring, 0);
"$(addSynapse, ($(id_pre) + 1) % $(num_post));\n"
"$(endRow);\n");
SET_CALC_MAX_ROW_LENGTH_FUNC([](unsigned int numPre, unsigned int numPost, const std::vector<double> &pars){ return 1;});
};
void modelDefinition(NNmodel &model)
{
// Settings
// definition of tenHHRing
model.setDT(0.1);
model.setName("tenHHRing");
7.15, // 0 - gNa: Na conductance in muS
50.0, // 1 - ENa: Na equi potential in mV
1.43, // 2 - gK: K conductance in muS
-95.0, // 3 - EK: K equi potential in mV
0.02672, // 4 - gl: leak conductance in muS
-63.563, // 5 - El: leak equi potential in mV
0.143); // 6 - Cmem: membr. capacity density in nF
-60.0, // 0 - membrane potential V
0.0529324, // 1 - prob. for Na channel activation m
0.3176767, // 2 - prob. for not Na channel blocking h
0.5961207); // 3 - prob. for K channel activation n
model.addNeuronPopulation<NeuronModels::TraubMiles>("Pop1", 10, p, ini);
WeightUpdateModels::StaticPulse::VarValues s_ini(
-0.2); // 0 - g: the synaptic conductance value
1.0, // 0 - tau_S: decay time constant for S [ms]
-80.0); // 1 - Erev: Reversal potential
"Pop1", "Pop1",
{}, s_ini,
ps_p, {},
initConnectivity<Ring>());
model.finalize();
}

We can now build our new model:

>> genn-buildmodel.sh tenHHRingModel.cc
Note
Again, if you don't have an NVIDIA GPU and are running GeNN in CPU_ONLY mode, you can instead build with the -c option as described in Tutorial 1.

Now we can open the tenHHRingSimulation.cc file and update the file name of the model includes to match the name we set previously:

// tenHHRingModel simulation code
#include "tenHHRing_CODE/definitions.h"

Additionally, we need to add a call to a second initialisation function to main() after we call initialize():

inittenHHRing();

This initializes any variables associated with the sparse connectivity we have added. Finally, after adjusting the GNUmakefile or MSBuild script to point to tenHHRingSimulation.cc rather than tenHHSimulation.cc, we can build and run our new simulator in the same way we did in Tutorial 1. However, even after all our hard work, if we plot the content of the first column against the subsequent 10 columns of tenHHexample.V.dat it looks very similar to the plot we obtained at the end of Tutorial 1.

tenHHRingexample1.png

This is because none of the neurons are spiking so there are no spikes to propagate around the ring.

Providing initial stimuli

We can use a NeuronModels::SpikeSource to inject an initial spike into the first neuron in the ring during the first timestep to start spikes propagating. Firstly we need to define another sparse connectivity initialisation snippet at the top of tenHHRingModel.cc which simply creates a single synapse on the first row of the synaptic matrix:

class FirstToFirst : public InitSparseConnectivitySnippet::Base
{
public:
DECLARE_SNIPPET(FirstToFirst, 0);
"if($(id_pre) == 0) {\n"
" $(addSynapse, $(id_pre));\n"
"}\n"
"$(endRow);\n");
SET_CALC_MAX_ROW_LENGTH_FUNC([](unsigned int, unsigned int, const std::vector<double> &){ return 1;});
};
IMPLEMENT_SNIPPET(FirstToFirst);

We then need to add it to the network by adding the following to the end of the modelDefinition(...) function:

"Stim", "Pop1",
{}, s_ini,
ps_p, {},
initConnectivity<FirstToFirst>());

and finally inject a spike in the first timestep (in the same way that the t variable is provided by GeNN to keep track of the current simulation time in milliseconds, iT is provided to keep track of it in timesteps):

if(iT == 0) {
spikeCount_Stim = 1;
spike_Stim[0] = 0;
#ifndef CPU_ONLY
pushStimSpikesToDevice();
#endif
}
Note
spike_Stim[n] is used to specify the indices of the neurons in population Stim spikes which should emit spikes where $n \in [0, \mbox{spikeCount\_Stim} )$.

At this point our user code tenHHRingModel.cc should look like this

// Model definintion file tenHHRing.cc
#include "modelSpec.h"
{
public:
DECLARE_SNIPPET(Ring, 0);
"$(addSynapse, ($(id_pre) + 1) % $(num_post));\n"
"$(endRow);\n");
SET_CALC_MAX_ROW_LENGTH_FUNC([](unsigned int, unsigned int, const std::vector<double> &){ return 1;});
};
class FirstToFirst : public InitSparseConnectivitySnippet::Base
{
public:
DECLARE_SNIPPET(FirstToFirst, 0);
"if($(id_pre) == 0) {\n"
" $(addSynapse, $(id_pre));\n"
"}\n"
"$(endRow);\n");
SET_CALC_MAX_ROW_LENGTH_FUNC([](unsigned int, unsigned int, const std::vector<double> &){ return 1;});
};
IMPLEMENT_SNIPPET(FirstToFirst);
void modelDefinition(NNmodel &model)
{
// definition of tenHHRing
model.setDT(0.1);
model.setName("tenHHRing");
7.15, // 0 - gNa: Na conductance in muS
50.0, // 1 - ENa: Na equi potential in mV
1.43, // 2 - gK: K conductance in muS
-95.0, // 3 - EK: K equi potential in mV
0.02672, // 4 - gl: leak conductance in muS
-63.563, // 5 - El: leak equi potential in mV
0.143); // 6 - Cmem: membr. capacity density in nF
-60.0, // 0 - membrane potential V
0.0529324, // 1 - prob. for Na channel activation m
0.3176767, // 2 - prob. for not Na channel blocking h
0.5961207); // 3 - prob. for K channel activation n
model.addNeuronPopulation<NeuronModels::TraubMiles>("Pop1", 10, p, ini);
WeightUpdateModels::StaticPulse::VarValues s_ini(
-0.2); // 0 - g: the synaptic conductance value
1.0, // 0 - tau_S: decay time constant for S [ms]
-80.0); // 1 - Erev: Reversal potential
"Pop1", "Pop1",
{}, s_ini,
ps_p, {},
initConnectivity<Ring>());
"Stim", "Pop1",
{}, s_ini,
ps_p, {},
initConnectivity<FirstToFirst>());
model.finalize();
}

and tenHHRingSimulation.cc` should look like this:

// Standard C++ includes
#include <fstream>
// tenHHRing simulation code
#include "tenHHRing_CODE/definitions.h"
int main()
{
allocateMem();
initialize();
inittenHHRing();
ofstream os("tenHHRing_output.V.dat");
while(t < 200.0f) {
if(iT == 0) {
glbSpkStim[0] = 0;
glbSpkCntStim[0] = 1;
#ifndef CPU_ONLY
pushStimSpikesToDevice();
#endif
}
#ifdef CPU_ONLY
stepTimeCPU();
#else
stepTimeGPU();
pullPop1StateFromDevice();
#endif
os << t << " ";
for (int j= 0; j < 10; j++) {
os << VPop1[j] << " ";
}
os << endl;
}
os.close();
return 0;
}

Finally if we build, make and run this model; and plot the first 200 ms of the ten neurons' membrane voltages - they now looks like this:

tenHHRingexample2.png

Previous | Top | Next