Exploring the dynamic range¶
In this example we’ll explore the dynamic range of synaptic stimulation.
We will
set up a network using PyNN
incrementally add more and more synaptic stimulation
quantify the effect of the synaptic stimulation on the membrane of different neurons for excitatory and inhibitory stimulation
In order to use the microscheduler we have to set some environment variables first:
from _static.common.helpers import setup_hardware_client
setup_hardware_client()
We’ll also configure matplotlib and import some tools.
%matplotlib inline
import numpy as np
import pandas as pd
import itertools
import matplotlib.pyplot as plt
from contextlib import suppress
with suppress(IOError):
plt.style.use("_static/matplotlibrc")
import pynn_brainscales.brainscales2 as pynn
Before we define our network, we load the default calibration.
A default calibration is generated for every setup every night. We save the nightly calibration in two variables such that we can use it later when we define our neuronal network.
from _static.common.helpers import get_nightly_calibration
calib = get_nightly_calibration()
Define network in PyNN¶
First, we will set up some variables determining the sweep we want to perform. The maximum weight of a single synapse is 63. Therefore we will add more projections if we exceed this maximum.
neurons = range(0, 512, 64)
max_weight = 63
max_number_of_projections = 5
weights = range(0, max_number_of_projections * max_weight, 32)
receptor_types = ["inhibitory", "excitatory"]
We will store the results in a dictionary.
from collections import defaultdict
results = defaultdict(list)
We configure the logger to print some output during the run.
log = pynn.logger.get("dynamic_range")
pynn.logger.set_loglevel(log, pynn.logger.LogLevel.INFO)
Next we run the network multiple times with various configurations.
from dlens_vx_v3 import hxcomm
# to speed things up, we keep the connection alive between experiments
with hxcomm.ManagedConnection() as connection:
for neuron, receptor_type in itertools.product(neurons, receptor_types):
# the neuronPerm
pynn.setup(connection=connection,
neuronPermutation=[neuron],
initial_config=calib)
# always have only 1 neuron
number_of_neurons = 1
population = pynn.Population(number_of_neurons,
pynn.cells.HXNeuron())
# disable spiking
population.set(threshold_enable=False)
# record the membrane voltage
population.record("v")
input_spiketimes = [0.5]
stimulus = pynn.Population(1,
pynn.cells.SpikeSourceArray(spike_times=input_spiketimes))
projections = []
# Adjust weights of existing projections and add new projections if the desired weight
# exceeds the maximum weight which can currently be implemented.
for w in weights:
needed_projections = int(np.ceil(w / max_weight))
new_projections = needed_projections - len(projections)
for _ in range(new_projections):
proj = pynn.Projection(stimulus,
population,
pynn.AllToAllConnector(),
receptor_type=receptor_type,
synapse_type=pynn.standardmodels.synapses.StaticSynapse(weight=0))
projections.append(proj)
sign = 1 if receptor_type == "excitatory" else -1
# projections with maximum weight
for proj in projections[:(w // max_weight)]:
proj.set(weight=sign * max_weight)
# projection with the remaining weight
if projections:
projections[-1].set(weight=sign * (w % max_weight))
pynn.run(1) # ms (hw)
membrane = population.get_data().segments[-1].irregularlysampledsignals[0]
min_membrane = float(membrane[10:].min())
max_membrane = float(membrane[10:].max())
results["weight"].append(w)
results["receptor_type"].append(receptor_type)
results["neuron"].append(neuron)
results["membrane_min"].append(min_membrane)
results["membrane_max"].append(max_membrane)
log.info(f"{neuron=} {receptor_type=} {w=} "
f"{min_membrane=} {max_membrane=}")
pynn.reset()
pynn.end()
log.info("experiment done")
df = pd.DataFrame.from_dict(results)
log.info("DataFrame created")
Now, all results are stored in the Pandas DataFrame that we can analyse with the code below. For the excitatory stimulation we plot the mean of the maximum of the membrane trace and the variance over the neurons. We do the same for the inhibitory stimulation but take the minimum of the membrane trace this time.
First we aggregate over the neurons and create convience columns for the analysis:
def aggregate(df):
return (df
.groupby(['receptor_type', 'weight'])
.agg(**{f"{method}_{col}" : (col, method) for col, method
in itertools.product(["membrane_min", "membrane_max"],
['mean', 'std'])})
.reset_index()
)
df_agg = aggregate(df)
Next we define a helper function to plot the variance over the neurons as an error band:
def plot_with_errorband(ax, x, y, error, label, color):
ax.plot(x, y, '-', color=color)
ax.fill_between(x, y-error, y+error, label=label, color=color, alpha=0.7)
Now we can do the final plot:
fig, ax = plt.subplots()
lookup = {'excitatory_color' : "tab:blue",
'inhibitory_color' : "tab:orange",
'excitatory_column' : "membrane_max",
'inhibitory_column' : "membrane_min"}
for receptor_type in ['excitatory', 'inhibitory']:
df_ = df_agg[df_agg.receptor_type==receptor_type]
color = lookup[f"{receptor_type}_color"]
column = lookup[f"{receptor_type}_column"]
plot_with_errorband(ax=ax,
x=df_["weight"],
y=df_[f"mean_{column}"],
error=df_[f"std_{column}"],
label=receptor_type,
color=color)
ax.set_xlabel("weight")
ax.set_ylabel("membrane min/max [MADC]")
ax.legend()