GeNN  4.9.0
GPU enhanced Neuronal Networks (GeNN)
Tutorial 2 (C++)

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");
};

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 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.

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::SPARSE_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. 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");
};
void modelDefinition(ModelSpec &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>());
}

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 (C++).

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():

initializeSparse();

This initializes any variables associated with the sparse connectivity we have added (and will also copy any manually initialised variables to the GPU). Then, after using the genn-create-user-project tool to create a new project with a model name of tenHHRing and using tenHHRingSimulation.cc rather than tenHHSimulation.cc, we can build and run our new simulator in the same way we did in Tutorial 1 (C++). 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 (C++).

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::SpikeSourceArray to inject an initial spike into the first neuron in the ring during the first timestep to start spikes propagating. We can do this by adding the following neuron and synapse populations to the network at the end of the modelDefinition(...) function:

uninitialisedVar(), // 0 - startSpike indices
uninitialisedVar()); // 1 - endSpike indices
model.addNeuronPopulation<NeuronModels::SpikeSourceArray>("Stim", 1, {}, stim_ini);
"Stim", "Pop1",
{}, s_ini,
ps_p, {},
initConnectivity<InitSparseConnectivitySnippet::OneToOne>());

Each neuron in the spike source array population has its own list of spikes which are concatenated together and stored in an array made accesible to all neurons in the population using an extra global parameter (see Extra global parameters). Although, in this model, there is only one neuron in the Stim population so startSpike and endSpike could be automatically initialised in the model description, typically they need to be initialised manually in the user-code so we mark these as uninitialised using uninitialisedVar(). We then connect the spike source array to Pop1 using another synapse population with sparse connectivity, initialised using the built in InitSparseConnectivitySnippet::OneToOne model so the single neuron in the Stim population is connected to the first neuron in the ring. Next, we add the code to initialise startSpike and endSpike to tenHHSimulation.cc, between the calls to initialize() and initializeSparse():

startSpikeStim[0] = 0;
endSpikeStim[0] = 1;

Finally, we also add code to allocate, intialise and push the spike times to the extra global parameter:

allocatespikeTimesStim(1);
spikeTimesStim[0] = 0.0f;
pushspikeTimesStimToDevice(1);

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");
};
void modelDefinition(ModelSpec &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
uninitialisedVar(), // 0 - startSpike indices
uninitialisedVar()); // 1 - endSpike indices
model.addNeuronPopulation<NeuronModels::TraubMiles>("Pop1", 10, p, ini);
model.addNeuronPopulation<NeuronModels::SpikeSourceArray>("Stim", 1, {}, stim_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<InitSparseConnectivitySnippet::OneToOne>());
}

and tenHHRingSimulation.cc` should look like this:

// Standard C++ includes
#include <fstream>
// tenHHRing simulation code
#include "tenHHRing_CODE/definitions.h"
int main()
{
allocateMem();
initialize();
startSpikeStim[0] = 0;
endSpikeStim[0] = 1;
initializeSparse();
allocatespikeTimesStim(1);
spikeTimesStim[0] = 0.0f;
pushspikeTimesStimToDevice(1);
std::ofstream os("tenHHRing_output.V.dat");
while(t < 200.0f) {
stepTime();
pullVPop1FromDevice();
os << t << " ";
for (int j= 0; j < 10; j++) {
os << VPop1[j] << " ";
}
os << std::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