Model of peripheral nerve with ephaptic coupling (Capllonch-Juan & Sepulveda 2020)

 Download zip file   Auto-launch 
Help downloading and running models
Accession:263988
We built a computational model of a peripheral nerve trunk in which the interstitial space between the fibers and the tissues is modelled using a resistor network, thus enabling distance-dependent ephaptic coupling between myelinated axons and between fascicles as well. We used the model to simulate a) the stimulation of a nerve trunk model with a cuff electrode, and b) the propagation of action potentials along the axons. Results were used to investigate the effect of ephaptic interactions on recruitment and selectivity stemming from artificial (i.e., neural implant) stimulation and on the relative timing between action potentials during propagation.
Reference:
1 . Capllonch-Juan M, Sepulveda F (2020) Modelling the effects of ephaptic coupling on selectivity and response patterns during artificial stimulation of peripheral nerves. PLoS Comput Biol 16:e1007826 [PubMed]
Citations  Citation Browser
Model Information (Click on a link to find other models with that property)
Model Type: Extracellular; Axon;
Brain Region(s)/Organism:
Cell Type(s): Myelinated neuron;
Channel(s):
Gap Junctions:
Receptor(s):
Gene(s):
Transmitter(s):
Simulation Environment: NEURON; Python;
Model Concept(s): Ephaptic coupling; Stimulus selectivity;
Implementer(s):
"""
Compare two datasets, or two simulations.
"""

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import argrelextrema
from collections import OrderedDict
from gi.repository import Gtk
import zipfile
import math
import json
import csv
import sys
import os



def round_up(x, order=1):
	""" Round a number up to the given order of magnitude """
	return order * math.ceil(float(x) / order)
def round_down(x, order=1):
	""" Round a number down to the given order of magnitude """
	return order * math.floor(float(x) / order)

# Cwd
cwd = os.getcwd()

# Items
cwd_items = os.listdir(cwd)

# List zip files
zipflist = [
		"revs_round_2_stim_set01_current_02000nA_EC.zip", 
		"revs_round_2_stim_set01_current_02000nA_noEC.zip"
	]

print(zipflist)

# Datasets where to store all data
datasets = OrderedDict()

# Initialize some variables that are needed for the graphs
vmin = 1e99
vmax = -1e99

# Dictionary that identifies the znames with the ephaptic coupling
znames = {}

# Iterate over zip files
for zf in zipflist:
	print(zf)
	zname = zf.replace(".zip", "")

	# Create a directory for it if it doesn't exist
	isincwd = zname in cwd_items

	if not isincwd:
		# Create the directory
		os.mkdir(zname)
		# Reference the folder
		zfolder = os.path.join(cwd, zname)
		# Extract everything in it
		zip_ref = zipfile.ZipFile(zf, 'r')
		zip_ref.extractall(zfolder)
		zip_ref.close()

	# Reference the folder
	zfolder = os.path.join(cwd, zname)

	# Now process data
	datasets[zname] = OrderedDict()

	# Axons' activity

	# Results folder
	results_folder = os.path.join(zfolder, "data/results")
	# List all the items in the results folder
	items = sorted(os.listdir(results_folder))
	# Select the csv files
	items = [item for item in items if ".csv" in item]
	# Select the axon recording files
	items = [item for item in items if item[:4] == "Axon"]

	# Array for the axons' activity maxima
	maxima = []

	# Time
	dt = 0.005

	# AP peak times
	appt_ = {}

	# AP peak places
	APpeakplace = {}

	# Flags indicating which axons did fire and which not
	hasAP = []

	# Voltage data
	data = {}

	# Iterate over the files and read them
	i = 0
	for filename in items:
		data[i] = {}
		with open(os.path.join(results_folder, filename), "r") as f:
			fr = csv.reader(f)
			for row in fr:
				if "NODE" in row[0]:
					data[i][key].append([float(x) for x in row[1:]])
				elif len(row) == 3:
					pass
				elif len(row) == 1:
					try:
						data[i][key] = np.array(data[i][key])
					except NameError:
						# There's no key yet
						pass
					key = row[0]
					data[i][key] = []

		# When the last key is read, don't forget storing it
		data[i][key] = np.array(data[i][key])
		del key
		# Store data in the dictionary
		i += 1

	# Check maxima and relevant stuff
	for i, data_ in data.items():
		axondata = data_["v"]
		# Check if the maximum is an AP
		maximum = axondata.max()
		maxima.append(maximum)
		if maximum > 0:
			# Regions where v is greater than 15
			whereAPs = np.where(axondata > 15)
			# Time when the first AP is fired (v rises above 15mV)
			when1stAP = whereAPs[1].min()
			where1stAP = whereAPs[0][np.where(whereAPs[1] == when1stAP)][0]
			segment_maxima = argrelextrema(axondata[where1stAP], np.greater)[0]
			# Local maxima
			local_maxima = axondata[where1stAP][segment_maxima]
			# Local maxima greater than 15 mV
			# IMPORTANT: I named the following variable when1stAP_ just so 
			# it doesn't overwrite when1stAP, but I can make it overwrite 
			# it if I want
			when1stAP_ = segment_maxima[np.where(local_maxima > 15)][0]
			APpeaktime = dt * (when1stAP - 1)
			APpeakplace[i] = where1stAP
			appt_[i] = APpeaktime
			hasAP.append(True)
		else:
			hasAP.append(False)
		i += 1

	# Maxima to array
	maxima = np.array(maxima)

	# AP peak times to array
	# And subtract the pulse delay from them
	appt_values = np.array(list(appt_.values())) - 0.01

	if len(appt_values) == 0:
		print("No axon fired an AP")
		sys.exit()

	# Delete the key for tidyness
	del key

	# Store everything in the datasets dictionary
	datasets[zname]["APpeakplace"] = APpeakplace
	datasets[zname]["appt_"] = appt_
	datasets[zname]["appt_values"] = appt_values
	datasets[zname]["hasAP"] = hasAP
	datasets[zname]["figrow"] = zipflist.index(zf)
	datasets[zname]["data"] = data
	if "_EC" in zname:
		datasets[zname]["title"] = "With Ephaptic \nCoupling (sEC)"
		datasets[zname]["histcolor"] = "r"
		znames["EC"] = zname
	elif "noEC" in zname:
		datasets[zname]["title"] = "Without Ephaptic \nCoupling (snoEC)"
		datasets[zname]["histcolor"] = "b"
		znames["noEC"] = zname

	# Common variables
	vmin = min(vmin, appt_values.min())
	vmax = max(vmax, appt_values.max())

########################################################################
# Process vext[1]

# Get the initial value (for it = 0 or 1), which is what is used for 
# extstim
extstim = {}
vext_EC = {}
vext_noEC = {}
vext_noEC_mod = {}
vext_diffs = {}
vm_EC = {}
vm_noEC = {}
# Numbers of fired axons
nfa_sEC = 0
nfa_snoEC = 0
# Newly fired axons
newlyfa_total = 0



# Choose the node in the middle, which is roughly the closest to the 
# stimulating pad (not really, because that is on the first of four 
# rings)

dataset = datasets[znames["EC"]]
data = dataset["data"]
APpeakplace = dataset["APpeakplace"]
APpeakplace_noEC = datasets[znames["noEC"]]["APpeakplace"]

# Get the fields
for i, variables in data.items():
	field_EC = variables['vext[1]']
	field_noEC = datasets[znames["noEC"]]["data"][i]['vext[1]']
	transmv_EC = variables['v']
	transmv_noEC = datasets[znames["noEC"]]["data"][i]['v']

	# Chose the node of Ranvier that lies closest to the source
	try:
		n = APpeakplace[i]
	except KeyError:
		# This axon has no AP in any simulation
		n = field_EC.shape[0] // 2
	else:
		# It has one in sEC, so now check if there is an AP in snoEC
		nfa_sEC += 1
		try:
			_ = APpeakplace_noEC[i]
		except KeyError:
			# This axon has no AP in snoEC
			# Save the axon
			newlyfa_total += 1

	# Count the number of fired axons in snoEC
	try:
		_ = APpeakplace_noEC[i]
	except KeyError:
		pass
	else:
		nfa_snoEC += 1

	# Store the data into the dictionaries
	vext_EC[i] = field_EC[n]
	vext_noEC[i] = field_noEC[n]
	# Modify field_noEC to chop the last time step of the pulse from it
	# This is a way to compute the difference between the fields from the two 
	# simulations without getting spurious values
	field_noEC_mod = np.zeros_like(field_noEC[n])
	where = np.where(field_noEC[n] < -5.e-1)[0][:-1]
	field_noEC_mod[where] = field_noEC[n][where]
	vext_noEC_mod[i] = field_noEC_mod

	# Go on with the others
	vm_EC[i] = transmv_EC[n]
	vm_noEC[i] = transmv_noEC[n]
	extstim[i] = vext_noEC[i][3]
	vext_diffs[i] = vext_EC[i] - vext_noEC_mod[i]

print("Fired axons in sEC: %i"%nfa_sEC)
print("Fired axons in snoEC: %i"%nfa_snoEC)
print("Total number of newly fired axons: %i"%newlyfa_total)
print("Sanity check: %i"%(nfa_sEC - nfa_snoEC))

# Compound variables

# vext_diffs from all the axons grouped in one array
vext_diffs_array = np.array([vd for vd in vext_diffs.values()])
# Limits
vext_diffs_min = vext_diffs_array.min()
vext_diffs_max = vext_diffs_array.max()
# Mean and standard deviation
vda_mean = vext_diffs_array.mean(axis=0)
vda_std = vext_diffs_array.std(axis=0)
# Percentiles
vda_perc25 = np.percentile(vext_diffs_array, 25, axis=0)
vda_perc75 = np.percentile(vext_diffs_array, 75, axis=0)
vda_perc10 = np.percentile(vext_diffs_array, 10, axis=0)
vda_perc90 = np.percentile(vext_diffs_array, 90, axis=0)

print("Peak of averaged fields strengths: %f mV"%vda_mean.min())
print("Peak of all fields strengths: %f mV"%vext_diffs_array.min())
########################################################################
# Provisional figures
plt.close('all')

# Cut the time domain in half
nt = len(vext_EC[0]) // 2
time = dt * np.arange(nt)

# Choose n*n random axons
n = 4
these_axons = np.random.choice(list(extstim.keys()), n * n)
print('randomly selected axons:')
print(these_axons)
# Choose one specific axon
chosen_axon = 507

########################################################################
# Show figure

plt.style.use('./PaperFig_1textwidth_v5.mplstyle')

fig, ax = plt.subplots(1, 3)
ax = ax.flatten()

# Potentials
ax[0].plot(time, vm_noEC[chosen_axon][:nt], c='b', alpha=0.5)
ax[0].plot(time, vm_EC[chosen_axon][:nt], c='k')
ax[1].plot(time, vext_noEC_mod[chosen_axon][:nt], c='b', alpha=0.5)
ax[1].plot(time, vext_EC[chosen_axon][:nt], c='k')
ax[0].set_ylabel(r"$\rm v_{m}$ $\rm (mV)$")
ax[1].set_ylabel(r"$\rm v_{E}$ $\rm (mV)$")
ax[0].set_yticks(np.arange(-80, 80, 40))
ytick_separation = 50
yticks = np.arange(
		round_down(vext_EC[chosen_axon][:nt].min(), order=10), 
		round_up(vext_EC[chosen_axon][:nt].max(), order=10) + ytick_separation, 
		ytick_separation
	)

ax[1].set_yticks(yticks)
ax[1].set_ylim(yticks.min(), yticks.max())

# Plot all the vext_diff vs time
for vd in vext_diffs.values():
	ax[2].plot(time, vd[:nt], c='k', alpha=0.2)
	ax[2].plot(time, vda_mean[:nt], c='r', lw=1., alpha=1.)
	ax[2].plot(time, vda_mean[:nt] - vda_std[:nt], c='r', lw=0.2, ls='-', alpha=0.3)
	ax[2].plot(time, vda_mean[:nt] + vda_std[:nt], c='r', lw=0.2, ls='-', alpha=0.3)
	ax[2].axvline(time[where[0]], ls='-', c='k', lw=0.2, alpha=0.3)
	ax[2].axvline(time[where[-1]], ls='-', c='k', lw=0.2, alpha=0.3)

ax[1].set_xlabel("Time " + r"$\rm (ms)$")
ax[2].set_ylabel(r"$\rm v_{E}$ Diff. $\rm (mV)$")
ytick_separation = 30
yticks = np.arange(
		round_down(vext_diffs_min, order=10), 
		round_up(vext_diffs_max, order=10) + ytick_separation, 
		ytick_separation
	)
ax[2].set_yticks(yticks)
xticks = np.arange(time.min(), time.max() + 0.1, 0.1)
xticklabels = ["%0.1f"%x for x in xticks]
ax[0].set_xticks(xticks)
ax[1].set_xticks(xticks)
ax[2].set_xticks(xticks)
ax[0].set_xticklabels(xticklabels)
ax[1].set_xticklabels(xticklabels)
ax[2].set_xticklabels(xticklabels)
ax[0].set_xlim(time.min(), time.max())
ax[1].set_xlim(time.min(), time.max())
ax[2].set_xlim(time.min(), time.max())

########################################################################
# Save and show

fig.savefig("fig4.png", dpi=300)
plt.show()