Getting Started

This tutorial introduces the basics of EvSpikeSim, from linking your project with the library to creating and running your first Spiking Neural Network (SNN).

EvSpikeSim C++

In this section, we describe how to link and compile your project with EvSpikeSim and create your first SNN in C++.

Compile for CPU

If you installed EvSpikeSim for CPU, you can compile and link your project with EvSpikeSim by simply adding the -levspikesim argument at linkage, such as:

g++ foo.cpp -std=c++17 -levspikesim

Note

EvSpikeSim internally uses C++17 features. Therefore, your code also needs to be compiled with C++17. This is done by adding the -std=c++17 argument to g++.

Compile for GPUs

If you installed EvSpikeSim for GPU, we recommend to compile all source files using nvcc and specify g++ as host compiler. You should then link EvSpikeSim -levspikesim argument at linkage, such as:

nvcc foo.cpp -ccbin g++ -std=c++17 -levspikesim

Includes and Namespace

Each class of EvSpikeSim is located in its own file under the evspikesim directory. Therefore, to include the SpikingNetwork class to a source file, add the following include directive:

#include <evspikesim/SpikingNetwork.h>

Some classes are contained into sub-directories. For example, the Layer class and its sub-classes are located in the Layers subdirectory, leading to the following include directive:

#include <evspikesim/Layers/Layer.h>

All the content provided by EvSpikeSim are contained within the library namespace EvSpikeSim to avoid any name conflict. To make your code short, you can redefine the namespace to a shorter version:

namespace sim = EvSpikeSim;

or remove the namespace:

using namespace EvSpikeSim;

In this tutorial, we will use the first method.

Spike

Event-based simulations are driven by events. In EvSpikeSim, those events are the spikes that are received and fired by neurons. Each spike event is represented by:

  • its location;

  • the time at which it occurs.

Such a representation of events is similar to the Address-Event Representation protocol (or AER) used for inter-chip communication between neuromorphic hardware.

In EvSpikeSim, spikes are represented by the Spike structure defined as:

struct Spike {
    unsigned int index; // Index (in the layer) of the neuron that fired the spike.
    float time; // Timing of the spike.
}

The index attribute is the index of the neuron that fired the spike, in the layer where the event occured. For example, index=2 if the third neuron of the layer fired the spike. The time attribute is simply the timing of the spike in seconds. Therfore, a spike can be instanciated as follows:

#include <evspikesim/Spike.h>

...

sim::Spike spike(42, 0.021);

This example, instanciates and construct a spike event at the neuron with index 42 and at time 0.021 seconds.

Moreover, spikes are comparable. By comparing spikes, only their timings are taken into account and not their indices. All the standard comparators are available, i.e. ==, !=, <, >, <=, >=.

Note

For the , == and , != operators, spike timing are compared with a time precision of Spike::epsilon (by default: 1e-6, or 1 μs). See C++ API documentation for more details.

Finally, spikes are printable on streams:

std::cout << spike << std::endl;

Spike Arrays

Spike trains are sequences of spikes. In EvSpikeSim, spike trains are represented by an array of Spike of type SpikeArray. The SpikeArray effectively stores and sorts in time all the spikes of a layer, facilitating iterations in chronological order during simulations.

To create a spike array, simply instanciate a new SpikeArray object as follows:

#include <evspikesim/SpikeArray.h>

...

sim::SpikeArray arr();

Alternatively, spike arrays can be created with a vectors of spikes indices and spike timings, such as:

std::vector<unsigned int> indices = {1, 42, 21};
std::vector<float> times = {0.030, 0.0017, 0.012};
sim::SpikeArray arr(indices, times);

which directly populates the array with new Spike objects.

To add a new spike, call the add method:

sim::SpikeArray arr();
sim::Spike spike(42, 0.021);

arr.add(spike);

The add method can also be called with vectors of spikes indices and spike timings, such as:

sim::SpikeArray arr();
std::vector<unsigned int> indices = {1, 42, 21};
std::vector<float> times = {0.030, 0.0017, 0.012};

arr.add(indices, times);

A spike array can be sorted in time by calling the sort method:

arr.sort();

Finally, spike arrays are printable on streams:

std::cout << arr << std::endl;

Create a Spiking Network

SNNs in EvSpikeSim are feedforward networks. This means that layers are successively simulated and do not form any cycle or recurrence.

To create a new (empty) SNN, instanciate a SpikingNetwork object:

#include <evspikesim/SpikingNetwork.h>

...

sim::SpikingNetwork net;

Add Layers of Spiking Neurons

Note

Only a single type of layer (FCLayer) is currently available in EvSpikeSim. New type of layer, such as convolution, will be added in future releases.

So far, SpikingNetwork object is empty and requires layers to do meaniningful computation. All layers in EvSpikeSim inherit from the Layer base class. To add layer a layer to it, call the add_layer method with a layer class as template and the arguments of the new layer’s constructor as parameters. For example, to add a layer of Fully-Connected (FC) neurons:

#include <evspikesim/SpikingNetwork.h>
#include <evspikesim/Layers/FCLayer.h>
#include <evspikesim/Initializers/UniformInitializer.h>

...

sim::SpikingNetwork net; // Network

unsigned int n_inputs = 100; // Number of inputs.
unsigned int n_neurons = 1000; // Number of neurons in the layer.
float tau_s = 0.010; // Synaptic time constant of 10 milliseconds.
float threshold = 0.1; // Threshold of the neurons.
sim::RandomGenerator gen; // Random generator for initializer
sim::UniformInitializer init(gen); // Uniform weight initializer

std::shared_ptr<sim::FCLayer> layer = net.add_layer<sim::FCLayer>(n_inputs, n_neurons, tau_s, threshold, init); // Add layer

Several parameters are required for the construction of a FCLayer object:

  • the number of input neurons, typically the number of neurons in the previous layer;

  • the number of neurons in the layer;

  • the synaptic time constant controlling the decay of the neurons;

  • the threshold of the neurons;

  • and a weight initializer.

Here, a new layer of 1000 fully-connected neurons receiving 100 inputs with a synaptic time constant of 10 milliseconds and a threshold of 0.1 is added to the network. Its weights are initialized with a uniform distribution between -1 and 1 (i.e. the default lower and upper bounds of UniformInitializer). Finally, the add_layer method returns a shared pointer on the newly created FCLayer object.

Access Layers

Layers in a spiking network can also be accessed as follows:

std::shared_ptr<sim::Layer> layer = net[0]; // Get the first layer

Also, the output layer, i.e. the last added layer, can be accessed using the get_output_layer method:

std::shared_ptr<sim::Layer> output_layer = net.get_output_layer(); // Get the output layer

Finally, layers are iterable:

for (std::shared_ptr<sim::Layer> it : net) {
    // Do something
}

Note

Note that, when accessing layers, the based class Layer is returned.

Access and Mutate Weights

Synapses of layers are stored in NDArray objects. This object can be accessed using the get_weights method of a layer. Taking the layer previously created, weights are accessed and mutated as follows:

sim::NDArray<float> &weights = layer->get_weights(); // Get the weight matrix of the layer.
std::vector<unsigned int> dims = weights.get_dims(); // Get the dimensions of the matrix.

float &w = weights.get(3, 5); // Get the weight of the connection.
w = 0.1 // Set -0.1 to the connection.
weights.set(-0.1, 3, 5); // Set -0.1 to the connection.

Here, we first get the weight matrix from the layer and its dimensions. We then get the weight between the post-synaptic neuron at index 3 and the pre-synaptic neuron at index 5. The last two lines show two different ways to set a new value to the connection.

Alternatively, a contiguous and mutable vector can be obtained from the weight matrix:

sim::NDArray<float> &weights = layer->get_weights(); // Get the weight matrix of the layer.
std::vector<unsigned int> dims = weights.get_dims(); // Get the dimensions of the matrix.

sim::vector<float> &weights_cont = weights.get_values(); // Get weights as a vector.

float w = weights[3 * dims[1] + 5] // Get the weight of the connection.
weights_cont[3 * dims[1] + 5] = -0.1; // Set -0.1 to the connection.

This has the same effect as the previous code but it requires indexing using the dimensions of the matrix.

Note

get_values returns a reference on a EvSpikeSim::vector object. In the CPU implementation, EvSpikeSim::vector is a standard std::vector. In the GPU implementation, this is a std::vector that uses a cuda managed pointer.

New weights can also be set with the = operator:

sim::NDArray<float> &weights = layer->get_weights(); // Get the weight matrix of the layer.
std::vector<float> new_weights = {1.0, 0.2, -1.0, ..., 0.5};
weights = new_weights;

Note

The vector of new weights must match the size of the current weights.

Run the SNN

After setting up the network, it is ready for inference. This is done by calling the infer method of SpikingNetwork with a sorted input SpikeArray:

std::vector<unsigned int> input_indices = {1, 42, 21};
std::vector<float> input_times = {0.030, 0.0017, 0.012};
sim::SpikeArray input_spikes(input_indices, input_times); // Create input spikes

input_spikes.sort(); // Sort spikes in time

const sim::SpikeArray &output_spikes = net.infer(input_spikes); // Infer the network

Note

Input spikes must be sorted in time before being sent for inference or infer will throw a runtime error.

Alternatively, input indices and times can directly be given as argument to infer:

std::vector<unsigned int> input_indices = {1, 42, 21};
std::vector<float> input_times = {0.030, 0.0017, 0.012};

const sim::SpikeArray &output_spikes = net.infer(input_indices, input_times); // Infer the network

This way, input indices and times do not have to be sorted.

Note

When passing indices and times as argument to infer, a SpikeArray is implicitly created and sorted in time.

After inference, post-synaptic spikes of hidden layers can be accessed as follows:

std::shared_ptr<sim::Layer> &layer = net[0];
const sim::SpikeArray &hidden_spikes = layer->get_post_spikes();

Additionally, neurons spike counts are also available:

std::shared_ptr<sim::Layer> &layer = net[0];
const sim::vector<unsigned int> &hidden_spike_counts = layer->get_n_spikes();

Full Example

#include <evspikesim/SpikingNetwork.h>
#include <evspikesim/Layers/FCLayer.h>
#include <evspikesim/Initializers/UniformInitializer.h>
#include <evspikesim/Misc/RandomGenerator.h>

namespace sim = EvSpikeSim;

int main() {
    // Create network
    sim::SpikingNetwork network;

    // Layer parameters
    unsigned int n_inputs = 10;
    unsigned int n_neurons = 100;
    float tau_s = 0.010;
    float threshold = 0.1;

    // Uniform distribution for weight initialization (by default: [-1, 1])
    sim::RandomGenerator gen;
    sim::UniformInitializer init(gen);

    // Add fully-connected layer to the network
    std::shared_ptr<sim::FCLayer> layer = network.add_layer<sim::FCLayer>(n_inputs, n_neurons, tau_s, threshold, init);

    // Create input spikes
    std::vector<unsigned int> input_indices = {0, 8, 2, 4};
    std::vector<float> input_times = {0.010, 0.012, 0.21, 0.17};

    // Inference
    auto output_spikes = network.infer(input_indices, input_times);

    // Print output spikes
    std::cout << "Output spikes:" << std::endl;
    std::cout << output_spikes << std::endl;

    // Print output spike counts
    std::cout << "Output spike counts:" << std::endl;
    for (auto it : layer->get_n_spikes())
        std::cout << it << " ";
    std::cout << std::endl;
    return 0;
}

EvSpikeSim Python

In this section, we describe how to import EvSpikeSim to your Python project and create your first SNN.

Import EvSpikeSim

We recommand to import EvSpikeSim as follows:

import evspikesim as sim

Spike

Event-based simulations are driven by events. In EvSpikeSim, those events are the spikes that are received and fired by neurons. Each spike event is represented by:

  • its location;

  • the time at which it occurs.

Such a representation of events is similar to the Address-Event Representation protocol (or AER) used for inter-chip communication between neuromorphic hardware.

In EvSpikeSim, spikes are represented by the Spike class. This class has two attributes:

  • An index attribute that is the index of the neuron that fired the spike, in the layer where the event occured. For example, index=2 if the third neuron of the layer fired the spike.

  • A time attribute that is the timing of the spike in seconds.

A spike can be instanciated as follows:

spike = sim.spike(42, 0.021)

This example, instanciates and construct a spike event at the neuron with index 42 and at time 0.021 seconds.

Moreover, spikes are comparable. By comparing spikes, only their timings are taken into account and not their indices. All the standard comparators are available, i.e. ==, !=, <, >, <=, >=.

Note

For the , == and , != operators, spike timing are compared with a time precision of Spike::epsilon (by default: 1e-6, or 1 μs). See Python API documentation for more details.

Finally, spikes are printable:

print(spike)

Spike Arrays

Spike trains are sequences of spikes. In EvSpikeSim, spike trains are represented by an array of Spike of type SpikeArray. The SpikeArray effectively stores and sorts in time all the spikes of a layer, facilitating iterations in chronological order during simulations.

To create a spike array, simply instanciate a new SpikeArray object as follows:

arr = sim.SpikeArray()

Alternatively, spike arrays can be created with a list of spikes indices and spike timings, such as:

arr = sim.SpikeArray(indices=[1, 42, 21], times=[0.030, 0.0017, 0.012])

or numpy ndarrays:

import numpy as np

...

indices = np.array([1, 42, 21], dtype=np.uint32)
times = np.array([0.030, 0.0017, 0.012], dtype=np.float32)
arr = sim.SpikeArray(indices=indices, times=times);

which both directly populate the array with new Spike objects.

Note

Numpy arrays of indices must be of dtype uint32 and arrays of timings must be of dtype float32. Numpy uses 64 bits values by default which is incompatible with the 32 bits values in EvSpikeSim.

To add a new spike, call the add method:

arr = sim.SpikeArray()

arr.add(index=42, time=0.021) # Add new spike

The add method can also be called with lists of spikes indices and spike timings, such as:

arr = sim.SpikeArray()

arr.add(indices=[1, 42, 21], times=[0.030, 0.0017, 0.012])  # Add new spikes

or with numpy arrays:

arr = sim.SpikeArray()

indices = np.array([1, 42, 21], dtype=np.uint32)
times = np.array([0.030, 0.0017, 0.012], dtype=np.float32)

arr.add(indices=indices, times=times)  # Add new spikes

A spike array can be sorted in time by calling the sort method:

arr.sort()

Finally, spike arrays are also printable:

print(arr)

Create a Spiking Network

SNNs in EvSpikeSim are feedforward networks. This means that layers are successively simulated and do not form any cycle or recurrence.

To create a new (empty) SNN, instanciate a SpikingNetwork object:

net = sim.SpikingNetwork()

Add Layers of Spiking Neurons

Note

Only a single type of layer (FCLayer) is currently available in EvSpikeSim. New type of layer, such as convolution, will be added in future releases.

So far, SpikingNetwork object is empty and requires layers to do meaniningful computation. All layers in EvSpikeSim inherit from the Layer base class. To add layer a Fully-Connected (FC) layer to it, call the add_fc_layer method with the following parameters:

net = sim.SpikingNetwork() # Network

init = sim.initializers.UniformInitializer()
net.add_fc_layer(n_inputs=100, n_neurons=1000, tau_s=0.010, threshold=0.1, initializer=init)

Several parameters are required for the construction of a FCLayer object:

  • the number of input neurons, typically the number of neurons in the previous layer;

  • the number of neurons in the layer;

  • the synaptic time constant controlling the decay of the neurons;

  • the threshold of the neurons;

  • and a weight initializer.

Here, a new layer of 1000 fully-connected neurons receiving 100 inputs with a synaptic time constant of 10 milliseconds and a threshold of 0.1 is added to the network. Its weights are initialized with a uniform distribution between -1 and 1 (i.e. the default lower and upper bounds of UniformInitializer). Finally, the add_fc_layer method returns the the newly created FCLayer object.

Access Layers

Layers in a spiking network can also be accessed as follows:

layer = net[0] # Get the first layer

Also, the output layer, i.e. the last added layer, can be accessed using the output_layer property:

output_layer = net.output_layer # Get the output layer

Finally, layers are iterable:

for layer in net:
    # Do something

Note

Note that, when accessing layers, the based class Layer is returned.

Access and Mutate Weights

Synapses of layers accessed and mutated through a numpy ndarray. This object can be accessed using the weights attribute of a layer. Taking the layer previously created, weights are accessed and mutated as follows:

weights = layer.weights # Get the weight matrix of the layer.
dims = layer.shape

w = weights[3, 5] # Get the weight of the connection.
weights[3, 5] = 0.1 # Set -0.1 to the connection.

Here, we first get the weight matrix from the layer and its dimensions. We then get the weight between the post-synaptic neuron at index 3 and the pre-synaptic neuron at index 5. The last line shows how to set a new value to the same connection.

Alternatively, new weights can be set by setting the weights property with a numpy array:

new_weights = np.random.uniform(size=(1000, 100)) # Create new weights

layer.weights = new_weights # Set weights

Warning

When setting new weights, the numpy array must match the current shape of the weights. No check is performed by EvSpikeSim.

Run the SNN

After setting up the network, it is ready for inference. This is done by calling the infer method of SpikingNetwork with a sorted input SpikeArray:

input_spikes = sim.SpikeArray(indices=[1, 42, 21], times=[0.030, 0.0017, 0.012]) # Create input spikes
input_spikes.sort() # Sort spikes in time

output_spikes = net.infer(input_spikes) # Infer the network

Note

Input spikes must be sorted in time before being sent for inference or infer will throw an exception.

Alternatively, lists of input indices and times can directly be given as argument to infer:

output_spikes = net.infer(indices=[1, 42, 21], times=[0.030, 0.0017, 0.012]) # Infer the network

or with numpy arrays:

indices = np.array([1, 42, 21], dtype=np.uint32)
times = np.array([0.030, 0.0017, 0.012], dtype=np.float32)
output_spikes = net.infer(indices=indices, times=times) # Infer the network

This way, input indices and times do not have to be sorted.

Note

When passing indices and times as argument to infer, a SpikeArray is implicitly created and sorted in time.

After inference, post-synaptic spikes of hidden layers can be accessed as follows:

layer = net[0]
hidden_spikes = layer.post_spikes;

Additionally, neurons spike counts are also available:

layer = net[0];
hidden_spike_counts = layer.n_spikes

Full Example

import evspikesim as sim

if __name__ == "__main__":
    # Create network
    network = sim.SpikingNetwork()

    # Uniform distribution for weight initialization (by default: [-1, 1])
    init = sim.initializers.UniformInitializer()

    # Add fully-connected layer to the network
    layer = network.add_fc_layer(n_inputs=10, n_neurons=100, tau_s=0.010, threshold=0.1, initializer=init)

    # Create input spikes
    input_indices = [0, 8, 2, 4]
    input_times = [1.0, 1.5, 1.2, 1.1]

    # Inference
    output_spikes = network.infer(input_indices, input_times)

    # Print output spikes
    print("Output spikes:")
    print(output_spikes)

    # Print output spike counts
    print("Output spike counts:")
    print(layer.n_spikes)