Current clamp ramp
We will cover how to interpret the ramp current injections in this chapter. You may notice there was no ramp current injection analysis chapter. That is because you already have all the tools to analyze ramp current injections so we will just focus on interpreting the data. There is some “analysis”.
Source
import json
import urllib
from collections import defaultdict
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from bokeh.io import output_notebook, show
from bokeh.layouts import column, gridplot, row
from bokeh.models import ColumnDataSource, CustomJS, Slider, Spinner
from bokeh.plotting import figure
from numpy.polynomial import Polynomial
from scipy import optimize, signal, stats
output_notebook()First we are going to load our data and define our analysis variables.
temp_path = cwd = Path.cwd().parent / "data/ramp"
msn_dict = {}
for index in range(135, 139):
with open(temp_path / f"{index}.json", "r") as rf:
temp = json.load(rf)
temp["array"] = np.array(temp["array"])
msn_dict[index] = temp
pyr_dict = {}
for index in range(1, 5):
with open(temp_path / f"{index}.json", "r") as rf:
temp = json.load(rf)
temp["array"] = np.array(temp["array"])
pyr_dict[index] = temp
x_array = np.arange(len(msn_dict[135]["array"])) / 10start = 3000
end = 40000
fs = 10000
pulse_amp = 300The first thing to do is look through your data just to see what it looks like. For reference the data in this tutorial is from MSN in the DMS of an adult mouse.
The recorded data is usually in mV, as is the case for this data.
There is a short baseline of about 300 ms before the current injection starts.
There is a ramp current injection that starts a 0 pA and goes up to 300 pA.
There is a point where the cell will spike.
There are 4 repeates or sweeps.
The voltage the cell shows before the spiking occurs is nonlinear. This is important because resistance is not linear even though our analysis makes the assumption that it is. With the ramp current injection this becomes very clear. Not all cells show as strong a nonlinear change in voltage as MSNs. Some cells like PV interneurons do not spike with slow ramp current injections due to depolarization block.
Source
# Initial data
source = ColumnDataSource(data={"x": x_array, "y": msn_dict[1]["array"]})
# Create a plot
plot = figure(
x_axis_label="Time (ms)", y_axis_label="Voltage (mV)", width=400, height=300
)
plot.line("x", "y", source=source, line_color="black")
spinner = Spinner(title="Acquisition", low=135, high=138, step=1, value=1, width=80)
# JavaScript callback to fetch JSON data and update plot
callback = CustomJS(
args=dict(source=source, spinner=spinner),
code="""
let val = spinner.value
let URL = `https://cdn.jsdelivr.net/gh/LarsHenrikNelson/PatchClampHandbook@main/data/current_clamp/${val}.json`
fetch(URL)
.then(response => response.json())
.then(data => {
source.data.y = data["array"];
source.change.emit();
})
.catch(error => console.error('Error fetching data:', error));
""",
)
# Add a button to trigger the callback
spinner.js_on_change("value", callback)
# Layout and show
layout = column(spinner, plot)
show(layout)---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In[4], line 2
1 # Initial data
----> 2 source = ColumnDataSource(data={"x": x_array, "y": msn_dict[1]["array"]})
3
4 # Create a plot
5 plot = figure(
KeyError: 1To begin analyzing the data we should first create a ramp pulse. Many papers will define the ramp by the start current injection which is usually 0 up to the final current injection and then how fast the current injection occurs.
ramp = np.zeros(x_array.size)
ramp[start:end] = np.linspace(0, 300, num=int(end - start))
fig, ax = plt.subplots(layout="constrained")
ax.set_xlabel("Time (ms)")
ax.set_ylabel("Current (pA)")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
print(f"Ramp rate: {300 / ((40000 - 3000) / 10000)} pA/s")
_ = ax.plot(x_array, ramp)We can do a similar analysis as in Current clamp square pulse: Part 1 to find peaks and the spike threshold. However, for this analysis we are going to focus on analyzing the first spikes only so that we can get rheobase and the membrane resistance. However, to find rheobase we will need to find the spike threshold.
Finding spike threshold¶
We can use a similar method, the third derivative to find the spike threshold as we did in the square pulse chapter. First we find the peaks then use that to find the first spike threshold.
for d in [msn_dict, pyr_dict]:
for value in d.values():
array = value["array"]
peaks, _ = signal.find_peaks(
array,
height=10,
prominence=10,
)
value["peaks"] = peaks
first = np.gradient(array)
second = np.gradient(first)
third = np.gradient(second)
value["threshold"] = np.argmax(third[start : peaks[0]]) + startFirst we should check to make sure our spike threshold looks good. I will skip plotting the pyramidal neurons for now.
Source
fig, axes = plt.subplots(nrows=2, ncols=2, layout="constrained")
for acq, ax in zip(msn_dict.values(), axes.flat):
threshold = acq["threshold"]
ax.plot(
x_array[: acq["peaks"][1]],
acq["array"][: acq["peaks"][1]],
color="black",
linewidth=1,
)
ax.plot(x_array[threshold], acq["array"][threshold], ".", color="red", linewidth=1)
ax.axis("off")Next we can calculate rheobase. For ramp current injections, rheobase is the current at which the spike threshold occurs.
rheobase = np.mean([ramp[i["threshold"]] for i in msn_dict.values()])
print(f"Rheobase: {rheobase} pA")Next we will plot the current vs the . To calculate we can baseline the acquisition by subtracting the mean of the baseline or by subtracting the minimum value. One important thing is that membrane resistance becomes nonlinear as the neuron gets close to the spike threshold. Another thing is that in this MSN, the membrane resistance increases linearly as we get farther from the baseline membrane voltage. To show that the relationship between membrane resistance and current, thus membrane resistance, becomes nonlinear. To show that we will run a regression for a small portion of the ramp current injection, from 0 pA to 50 pA. We will also compare the MSN to a layer 5 pyramidal cell.
Source
reg_end = np.searchsorted(ramp, 50)
for d in [msn_dict, pyr_dict]:
for value in d.values():
array = value["array"]
threshold = acq["threshold"]
temp = array - array[:3000].mean()
output = stats.linregress(ramp[start:reg_end], temp[start:reg_end])
value["slope"] = output.slope
value["intercept"] = output.interceptSource
fig = plt.figure(layout="constrained", figsize=(10, 4))
subfigs = fig.subfigures(1, 2)
title = {0: "MSN", 1: "Pyr"}
for index, cell_dict in enumerate([msn_dict, pyr_dict]):
ax = subfigs[index].subplots(1, 1)
_ = subfigs[index].suptitle(title[index])
for acq in cell_dict.values():
threshold = acq["threshold"]
temp = acq["array"] - acq["array"][:3000].mean()
_ = ax.plot(
ramp[start:threshold], temp[start:threshold], color="black", alpha=0.5
)
_ = ax.plot(
ramp[start:threshold],
acq["slope"] * ramp[start:threshold] + acq["intercept"],
color="magenta",
)
_ = ax.set_xlabel("Current (pA)")
_ = ax.set_ylabel(r"$\Delta V$")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)You will notice two things. One is that the both MSNs and pyramidal neurons deviate a lot from the regression line the closer they get to the spike threshold. You will also notice that MSNs deviate even earlier even though their rheobase is a little bit higher. The reason is that MSNs have the inwardly rectifying potassium channels that close as the neuron gets closer to the spike threshold thus driving up the input resistance and increasing the rate of voltage change.
We can also calculate the instantaneous membrane resistance. I skip the first 1 ms second of the ramp when calculating the instantaneous resistance since there is a lot of noise.
Source
fig = plt.figure(layout="constrained", figsize=(10, 4))
subfigs = fig.subfigures(1, 2)
title = {0: "MSN", 1: "Pyr"}
for index, cell_dict in enumerate([msn_dict, pyr_dict]):
ax = subfigs[index].subplots(1, 1)
_ = subfigs[index].suptitle(title[index])
for acq in cell_dict.values():
threshold = acq["threshold"]
temp = acq["array"] - acq["array"][:3000].mean()
_ = ax.plot(
ramp[start + 1000 : threshold],
temp[start+1000: threshold] / ramp[start + 1000 : threshold] * 1000,
color="black",
alpha=0.5,
)
_ = ax.axhline(acq["slope"]*1000, color="magenta")
_ = ax.set_xlabel("Current (pA)")
_ = ax.set_ylabel(r"MOhm")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)You will notice that the slope we measured previously does not fit the MSNs instantaneous resistance very well where as the pyramidal cell has a better fit.
Lastly, lets look at how the instantaneous membrane resistance tracks with the membrane voltage.
Source
fig = plt.figure(layout="constrained", figsize=(10, 4))
subfigs = fig.subfigures(1, 2)
title = {0: "MSN", 1: "Pyr"}
for index, cell_dict in enumerate([msn_dict, pyr_dict]):
ax = subfigs[index].subplots(1, 1)
_ = subfigs[index].suptitle(title[index])
for acq in cell_dict.values():
threshold = acq["threshold"]
temp = acq["array"] - acq["array"][:3000].mean()
_ = ax.plot(
acq["array"][start+1000: threshold], temp[start+1000: threshold] / ramp[start + 1000 : threshold]*1000,
color="black",
alpha=0.5,
)
_ = ax.set_xlabel("Voltage (mV)")
_ = ax.set_ylabel(r"MOhm")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)We can see that similar to what we saw with the current injection, the membrane resistance also increases with membrane voltage.
Now what if I told you this feature of increasing resistance is really just an “artifact”. It is important to remember that the more open channels a cell has the lower its resistance should be. However, that strongly depends on the type of current. The current from the voltage-gated sodium channels adds on to our injected current because the voltage-gated sodium channels allow positive current the cell and we are injecting positive current. So even though conductance increases due to sodium channels opening and true membrane resistance should decrease, our membrane resistance looks higher because the sodium channel current is adding onto the current injection. Lastly, this brings us to an important aspect of neural function. There is the “physical” membrane resistance which is primarily determined by the number of open channels in the membrane. We could find this by blocking all the voltage-gated channels. Then there is the “functional” resistance which is directly related to conductance. Conductances can be positive or negative thus adding or subtracting to our current injection. The “functional” resistance depends on the current membrane voltage and the speed at which the membrane voltage is changing.