Introduction to the non-interactive queue runner

Welcome to this tutorial of using pyNN for the BrainScaleS-2 neuromorphic accelerator with the non-interactive queue runner. We will guide you through all the steps necessary to interact with the system and help you explore the capabilities of the on-chip analog neurons and synapses. A tutorial of the interactive usage can be found in BrainScaleS-2 single neuron experiments.

!pip install -U hbp_neuromorphic_platform
!pip install ebrains-drive
%matplotlib inline
import nmpi
client = nmpi.Client()
import time
import os
import ebrains_drive
import requests
from ebrains_drive.client import DriveApiClient

The next cell is a workaround for a missing functionality. Just run and ignore…

# Define helpers to store and extract required information of the currently used collab
class RepositoryInformation:
    def __init__(self, repository, repoInfo):
        self.repository = repository
        self.nameInTheUrl = repoInfo["name"]
        self.repoInfo = repoInfo

    def toString(self):
        return "nameInTheUrl=" + self.nameInTheUrl + ", full name=" + \
            self.repository.name + ", id=" + self.repository.id


def findRepositoryInfoFromDriveDirectoryPath(homePath):
    # Remove directory structure and subsequent folder names to extract the name of the collab
    name = homePath.replace("/mnt/user/shared/", "")
    if name.find("/") > -1:
        name = name[:name.find("/")]
    bearer_token = clb_oauth.get_token()
    ebrains_drive_client = ebrains_drive.connect(token=bearer_token)
    repo_by_title = ebrains_drive_client.repos.get_repos_by_filter("name", name)
    if len(repo_by_title) != 1:
        raise Exception("The repository for the collab name", name,
                        "can not be found.")

    # With the repo_by_title we can get the drive ID
    driveID = repo_by_title[0].id

    # and then we can use the driveID to look up the collab
    url = "https://wiki.ebrains.eu/rest/v1/collabs?driveId=" + driveID
    response = requests.get(
        url,
        headers={'Authorization': 'Bearer %s' % bearer_token})
    repo_info = response.json()
    return RepositoryInformation(repo_by_title[0], repo_info)


# Generate HBP client used to communicate with the hardware and extract
# collab information from current working directory using the previously
# defined helpers
client = nmpi.Client()
dir =!pwd
repoInfo = findRepositoryInfoFromDriveDirectoryPath(dir[0])

# Optionally: Set 'checkForQuota' to True to check if the currently used
# collab has an existing quota
checkForQuota = False
if checkForQuota:
    a = client.list_resource_requests(repoInfo.nameInTheUrl)
    anyAccepted = False
    if len(a) == 0:
        print("This collab does not have any quota entry yet. Request a test quota. "
              "This request will need to be reviewd and granted by an admin.")
        client.create_resource_request(
            title="Test quota request for " + repoInfo.nameInTheUrl,
            collab_id=repoInfo.nameInTheUrl,
            abstract="Test quota request",
            submit=True)
    else:
        for entry in a:
            if entry["status"] == "accepted":
                print("An accepted quota request exists")
                anyAccepted = True
        if not anyAccepted:
            print("A quota request is present, but it has not yet been granted.")
    if not anyAccepted:
        raise Exception(
            "This collab does not yet have an accepted quota entry. "
            "Therefore submitting jobs will not yet work.")

Next, we define a function which is used to execute the experiments on the BrainScaleS-2 neuromorphic accelerator.

from _static.common.collab_helpers import check_kernel

# Check if compatible kernel version is used
check_kernel()
kernel_env = os.getenv("LAB_KERNEL_NAME")
if "experimental" in kernel_env:
    software_version = "experimental"
else:
    software_version = "stable"

# Directory where we save the experiment results
outputDir = os.path.expanduser("~")

def execute_on_hardware(script_name):
    """
    Sends the provided script to the local cluster, where it is scheduled and executed on
    the neuromorphic chip. The result files are then loaded back into the collaboratory.
    :param script_name: Name of the script which gets executed
    :returns: Job id of executed job
    """
    collab_id = repoInfo.nameInTheUrl

    hw_config = {'SOFTWARE_VERSION': software_version}

    StartAt=time.time()
    # if connection broken, you need a new token (repeat the steps above)
    job = client.submit_job(source="~/"+script_name,
                          platform=nmpi.BRAINSCALES2,
                          collab_id=collab_id,
                          config=hw_config,
                          command="run.py",
                          wait=True)

    timeUsed = time.time() - StartAt
    job_id = job['id']
    print(str(job_id) + " time used: " + str(timeUsed))
    filenames = client.download_data(job, local_dir=os.path.expanduser("~"))
    print("All files: ",filenames)
    return job_id

First Experiment

Now we can start with the first experiment. Here, we will record the membrane of a single, silent neuron on the analog substrate. We save our experiment description to a python file and will send this file to a cluster in Heidelberg. In Heidelberg the experiment is executed on the BrainScaleS-2 neuromorphic system, the results are collected and send back.

%%file ~/first_experiment.py
# This first line is used to instruct the notebook not to execute the cell
# but to write its content into a file.
from neo.io import PickleIO
import pynn_brainscales.brainscales2 as pynn
# To begin with, we configure the logger used during our experiments

pynn.logger.default_config(level=pynn.logger.LogLevel.INFO)
logger = pynn.logger.get("single_neuron_demo")

# The pyNN-interface can be used similarly to existing simulators and other
# neuromorphic platforms.
pynn.setup()
# In the current state, we only expose the neuron type 'HXNeuron',
# which allows low-level access to all circuit parameters. It can be
# configured by passing initial values to the respective Population.
# Each population may consist of multiple neurons (in this case: one),
# all sharing the same parameters.
# Circuit parameters control the dynamic behaviour of the neuron as well as
# static configuration. Most of them are either boolean or given in units of
# 'LSB' for chip-internal Digital-to-Analog converters - they have no direct
# biological translation.
# For this first example, you may alter the leak potential and observe
# the response of the analog neuron's resting potential.
pop = pynn.Population(1, pynn.cells.HXNeuron(
                      # Leak potential, range: 300-1000
                      leak_v_leak=700,
                      # Leak conductance, range: 0-1022
                      leak_i_bias=1022)
                 )
# The chip contains a fast Analog-to-Digital converter. It can be used to
# record different observables of a single analog neuron - most importantly
# the membrane potential.
#
# The chip additionally includes slower, parallel ADCs which will allow for
# parallel access to analog signals in multiple neurons. Support for this
# ADC will be integrated in future versions of our pyNN-Api.
pop.record(["v"])

# Calling pynn.run(time_in_ms) will as a first step apply the static
# configuration to the neuromorphic substrate. As a second step, the network
# is evolved for a given amount of time and neurons are stimulated by any
# stimuli specified by the user.
# The time is given in units of milliseconds (wall clock time),
# representing the hardware's intrinsic 1000-fold speed-up compared to
# biological systems.
pynn.run(0.2)

# Store results to disk
PickleIO(filename='first_experiment.dat').write_block(pop.get_data("v"))

# Reset the pyNN internal state and prepare for the following experiment
pynn.end()
first_experiment_id = execute_on_hardware('first_experiment.py')

The following helper function plots the membrane potential as well as any spikes found in the result file of the experiment. It will be used throughout this tutorial.

import matplotlib.pyplot as plt
import numpy as np
from neo.io import PickleIO

def plot_membrane_dynamics(path : str):
    """
    Load the neuron data from the given path and plot the membrane potential
    and spiketrain of the first neuron.
    :param path: Path to the result file
    """
    # Experimental results are given in the 'neo' data format, the following
    # lines extract membrane traces as well as spikes and construct a simple
    # figure.
    block = PickleIO(path).read_block()
    for segment in block.segments:
        if len(segment.irregularlysampledsignals) != 1:
            raise ValueError("Plotting is supported for populations of size 1.")
        mem_v = segment.irregularlysampledsignals[0]
        try:
            for spiketime in segment.spiketrains[0]:
                plt.axvline(spiketime, color="black")
        except IndexError:
            print("No spikes found to plot.")
        plt.plot(mem_v.times, mem_v, alpha=0.5)
    plt.xlabel("Wall clock time [ms]")
    plt.ylabel("ADC readout [a.u.]")
    plt.ylim(0, 1023)  # ADC precision: 10bit -> value range: 0-1023
    plt.show()

Plot the results of the first experiment.

plot_membrane_dynamics(f"{outputDir}/job_{first_experiment_id}/first_experiment.dat")

Second Experiment

As a second experiment, we will let the neurons on BrainScaleS-2 spike by setting a ‘leak-over-threshold’ configuration.

%%file ~/second_experiment.py
from neo.io import PickleIO
import pynn_brainscales.brainscales2 as pynn

pynn.logger.default_config(level=pynn.logger.LogLevel.INFO)
logger = pynn.logger.get("single_neuron_demo")

pynn.setup()
# Since spiking behavior requires the configuration of additional circuits
# in the neuron, the initial values for our leak-over-threshold population
# are more complex.
# The different potentials (leak, reset, threshold) have no direct
# correspondence: A configured leak potential of 300 might equal a
# configured threshold potential of value 600 in natural units on the physical
# system.
pop = pynn.Population(1, pynn.cells.HXNeuron(
                          # Leak potential, range: 300-1000
                          leak_v_leak=1000,
                          # Leak conductance, range: 0-1022
                          leak_i_bias=200,
                          # Threshold potential, range: 0-600
                          threshold_v_threshold=300,
                          # Reset potential, range: 300-1000
                          reset_v_reset=400,
                          # Membrane capacitance, range: 0-63
                          membrane_capacitance_capacitance=63,
                          # Refractory time, range: 0-255
                          refractory_period_refractory_time=120,
                          # Enable reset on threshold crossing
                          threshold_enable=True,
                          # Reset conductance, range: 0-1022
                          reset_i_bias=1022,
                          # Enable strengthening of reset conductance
                          reset_enable_multiplication=True
                      ))
pop.record(["v", "spikes"])
pynn.run(0.2)

# Store results to disk
PickleIO(filename='second_experiment.dat').write_block(pop.get_data())
pynn.end()

Execute the experiment on the neuromorphic hardware and plot the results.

print("Starting 2nd experiment at ",time.ctime())
second_experiment_id = execute_on_hardware('second_experiment.py')
print("Start Plotting:")
plot_membrane_dynamics(f"{outputDir}/job_{second_experiment_id}/second_experiment.dat")

Third Experiment: Fixed-pattern noise

Due to the analog nature of the BrainScaleS-2 platform, the inevitable mismatch of semiconductor fabrication results in inhomogeneous properties of the computational elements. We will visualize these effects by recording the membrane potential of multiple neurons in leak-over-threshold configuration. You will notice different resting, reset and threshold potentials as well as varying membrane time constants.

%%file ~/third_experiment.py
from neo.io import PickleIO
import pynn_brainscales.brainscales2 as pynn

pynn.logger.default_config(level=pynn.logger.LogLevel.INFO)
logger = pynn.logger.get("single_neuron_demo")

pynn.setup()
num_neurons = 10
p = pynn.Population(num_neurons, pynn.cells.HXNeuron(
                        # Leak potential, range: 300-1000
                        leak_v_leak=1000,
                        # Leak conductance, range: 0-1022
                        leak_i_bias=200,
                        # Threshold potential, range: 0-600
                        threshold_v_threshold=300,
                        # Reset potential, range: 300-1000
                        reset_v_reset=400,
                        # Membrane capacitance, range: 0-63
                        membrane_capacitance_capacitance=63,
                        # Refractory time, range: 0-255
                        refractory_period_refractory_time=120,
                        # Enable reset on threshold crossing
                        threshold_enable=True,
                        # Reset conductance, range: 0-1022
                        reset_i_bias=1022,
                        # Enable strengthening of reset conductance
                        reset_enable_multiplication=True
                   ))
for neuron_id in range(num_neurons):
    logger.INFO(f"Recording analog variations: Run {neuron_id}")
    # Remove recording of previously selected neuron
    p.record(None)
    # Record neuron with current neuron id
    p_view = pynn.PopulationView(p, [neuron_id])
    p_view.record(["v"])
    pynn.run(0.1)
    pynn.reset()

# Store results to disk
PickleIO(filename='third_experiment.dat').write_block(p.get_data())

Execute the experiment on the neuromorphic hardware and plot the results.

third_experiment_id = execute_on_hardware('third_experiment.py')
print("Start Plotting:")
plot_membrane_dynamics(f"{outputDir}/job_{third_experiment_id}/third_experiment.dat")

The plot shows the recorded membrane traces of multiple different neurons. Due to the time-continuous nature of the system, there is no temporal alignment between the individual traces, so the figure shows multiple independent effects: * Temporal misalignment: From the system’s view, the recording happens in an arbitrary time frame during the continuously evolving integration. Neurons are not synchronized to each other. * Circuit-level mismatch: Each individual neurons shows slightly different analog properties. The threshold is different for all traces; as is the membrane time constant (visible as slope) and the reset potentials (visible as plateaus during the refractory time).

Fourth Experiment: External stimulation

Up to now, we have observed analog neurons without external stimulus. In this experiment, we will introduce the latter and examine post-synaptic pulses on the analog neuron’s membrane.

%%file ~/fourth_experiment.py
from neo.io import PickleIO
import pynn_brainscales.brainscales2 as pynn
from pynn_brainscales.brainscales2.standardmodels.synapses import StaticSynapse

pynn.logger.default_config(level=pynn.logger.LogLevel.INFO)
logger = pynn.logger.get("single_neuron_demo")

pynn.setup()

# Preparing the neuron to receive synaptic inputs requires the configuration
# of additional circuits. The additional settings include technical parameters
# for bringing the circuit to its designed operating point as well as
# configuration with a direct biological equivalent.
stimulated_p = pynn.Population(1, pynn.cells.HXNeuron(
                                   # Leak potential, range: 300-1000
                                   leak_v_leak=400,
                                   # Leak conductance, range: 0-1022
                                   leak_i_bias=200,
                                   # Threshold potential, range: 0-600
                                   threshold_v_threshold=400,
                                   # Reset potential, range: 300-1000
                                   reset_v_reset=300,
                                   # Membrane capacitance, range: 0-63
                                   membrane_capacitance_capacitance=63,
                                   # Refractory time, range: 0-255
                                   refractory_period_refractory_time=120,
                                   # Enable reset on threshold crossing
                                   threshold_enable=True,
                                   # Reset conductance, range: 0-1022
                                   reset_i_bias=1022,
                                   # Enable strengthening of reset conductance
                                   reset_enable_multiplication=True,
                                   # -- Parameters for synaptic inputs -- #
                                   # Enable synaptic stimulation
                                   excitatory_input_enable=True,
                                   inhibitory_input_enable=True,
                                   # Strength of synaptic inputs
                                   excitatory_input_i_bias_gm=1022,
                                   inhibitory_input_i_bias_gm=1022,
                                   # Synaptic time constants
                                   excitatory_input_i_bias_tau=200,
                                   inhibitory_input_i_bias_tau=200,
                                   # Technical parameters
                                   excitatory_input_i_shift_reference=300,
                                   inhibitory_input_i_shift_reference=300))
stimulated_p.record(["v", "spikes"])

# Create off-chip populations serving as excitatory external spike sources
exc_spiketimes = [0.01, 0.05, 0.080]
exc_stim_pop = pynn.Population(1, pynn.cells.SpikeSourceArray,
                               cellparams=dict(spike_times=exc_spiketimes))

# We represent projections as entries in the synapse matrix on the neuromorphic
# chip. Weights are stored in digital 6bit values (plus sign), the value
# range for on-chip weights is therefore -63 to 63.
# With this first projection, we connect the external spike source to the
# observed on-chip neuron population.
pynn.Projection(exc_stim_pop, stimulated_p,
                pynn.AllToAllConnector(),
                synapse_type=StaticSynapse(weight=63),
                receptor_type="excitatory")

# Create off-chip populations serving as inhibitory external spike sources
inh_spiketimes = [0.03]
inh_stim_pop = pynn.Population(1, pynn.cells.SpikeSourceArray,
                               cellparams=dict(spike_times=inh_spiketimes))
pynn.Projection(inh_stim_pop, stimulated_p,
                pynn.AllToAllConnector(),
                synapse_type=StaticSynapse(weight=-42),
                receptor_type="inhibitory")

# You may play around with the parameters in this experiment to achieve
# different traces. Try to stack multiple PSPs, try to make the neurons spike,
# try to investigate differences between individual neuron instances,
# be creative!
pynn.run(0.1)
# Store results to disk
PickleIO(filename='fourth_experiment.dat').write_block(stimulated_p.get_data())

Execute the experiment on the neuromorphic hardware and plot the results.

print("Starting 4th experiment at ",time.ctime())
fourth_experiment_id = execute_on_hardware('fourth_experiment.py')
print("Start Plotting:")
plot_membrane_dynamics(f"{outputDir}/job_{fourth_experiment_id}/fourth_experiment.dat")