From 4fbe46faad0db25430fe324e725b6954c19dcc2b Mon Sep 17 00:00:00 2001
From: Espen Hagen <2492641+espenhgn@users.noreply.github.com>
Date: Mon, 4 Oct 2021 09:57:51 +0200
Subject: [PATCH] Add example based on LFPykit example Example_Arbor_swc.ipynb
 (#1652)

---
 python/example/example_requirements.txt       |   2 +
 .../single_cell_extracellular_potentials.py   | 344 ++++++++++++++++++
 scripts/run_python_examples.sh                |   3 +
 3 files changed, 349 insertions(+)
 create mode 100644 python/example/example_requirements.txt
 create mode 100644 python/example/single_cell_extracellular_potentials.py

diff --git a/python/example/example_requirements.txt b/python/example/example_requirements.txt
new file mode 100644
index 00000000..9ead4f69
--- /dev/null
+++ b/python/example/example_requirements.txt
@@ -0,0 +1,2 @@
+# pip requirements file
+LFPykit>=0.3
diff --git a/python/example/single_cell_extracellular_potentials.py b/python/example/single_cell_extracellular_potentials.py
new file mode 100644
index 00000000..21508c41
--- /dev/null
+++ b/python/example/single_cell_extracellular_potentials.py
@@ -0,0 +1,344 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Example utilizing the **`LFPykit`** module (https://lfpykit.readthedocs.io,
+# https://github.com/LFPy/LFPykit) for predictions of extracellular
+# potentials using the line source approximation implementation
+# `LineSourcePotential` with a passive neuron model set up in Arbor
+# (https://arbor.readthedocs.io, https://github.com/arbor-sim/arbor).
+#
+# The neuron receives sinusoid synaptic current input in one arbitrary
+# chosen control volume (CV).
+# Its morphology is defined in the file `single_cell_detailed.swc`
+
+# import modules
+import sys
+import numpy as np
+import arbor
+import lfpykit
+import matplotlib.pyplot as plt
+from matplotlib.gridspec import GridSpec
+from matplotlib.collections import PolyCollection
+import pandas as pd
+
+
+class Recipe (arbor.recipe):
+    def __init__(self, cell):
+        super().__init__()
+
+        self.the_cell = cell
+
+        self.vprobe_id = (0, 0)
+        self.iprobe_id = (0, 1)
+        self.cprobe_id = (0, 2)
+
+        self.the_props = arbor.neuron_cable_properties()
+        self.the_cat = arbor.default_catalogue()
+        self.the_props.register(self.the_cat)
+
+    def num_cells(self):
+        return 1
+
+    def num_sources(self, gid):
+        return 0
+
+    def cell_kind(self, gid):
+        return arbor.cell_kind.cable
+
+    def cell_description(self, gid):
+        return self.the_cell
+
+    def global_properties(self, kind):
+        return self.the_props
+
+    def probes(self, gid):
+        return [
+            arbor.cable_probe_membrane_voltage_cell(),
+            arbor.cable_probe_total_current_cell(),
+            arbor.cable_probe_stimulus_current_cell()
+        ]
+
+
+# Read the SWC filename from input
+# Example from docs: single_cell_detailed.swc
+if len(sys.argv) < 2:
+    print("No SWC file passed to the program")
+    sys.exit(0)
+
+filename = sys.argv[1]
+
+# define morphology (needed for arbor.place_pwlin)
+morphology = arbor.load_swc_arbor(filename)
+
+# number of CVs per branch
+nseg = 3
+
+# Label dictionary
+defs = {}
+labels = arbor.label_dict(defs)
+
+# decor
+decor = arbor.decor()
+
+# set initial voltage, temperature, axial resistivity, membrane capacitance
+decor.set_property(
+    Vm=-65,  # Initial membrane voltage [mV]
+    tempK=300,  # Temperature [Kelvin]
+    rL=10000,  # Axial resistivity [Ω cm]
+    cm=0.01,  # Membrane capacitance [F/m**2]
+)
+
+# set passive mechanism all over
+# passive mech w. leak reversal potential (mV)
+pas = arbor.mechanism('pas/e=-65')
+pas.set('g', 0.0001)  # leak conductivity (S/cm2)
+decor.paint('(all)', pas)
+
+# set sinusoid input current at mid point of terminating CV (segment)
+iclamp = arbor.iclamp(5,  # stimulation onset (ms)
+                      1E8,  # stimulation duration (ms)
+                      -0.001,  # stimulation amplitude (nA)
+                      frequency=0.1,  # stimulation frequency (kHz)
+                      phase=0)  # stimulation phase)
+try:
+    # arbor >= 0.5.2 fix
+    decor.place('(location 4 0.16667)', iclamp, '"iclamp"')
+except TypeError:
+    decor.place('(location 4 0.16667)', iclamp)
+
+# number of CVs per branch
+policy = arbor.cv_policy_fixed_per_branch(nseg)
+decor.discretization(policy)
+
+# create cell and set properties
+cell = arbor.cable_cell(morphology, labels, decor)
+
+# create single cell model
+model = arbor.single_cell_model(cell)
+
+# instantiate recipe with cell
+recipe = Recipe(cell)
+
+# instantiate simulation
+context = arbor.context()
+domains = arbor.partition_load_balance(recipe, context)
+sim = arbor.simulation(recipe, domains, context)
+
+# set up sampling on probes
+schedule = arbor.regular_schedule(0.1)
+v_handle = sim.sample(recipe.vprobe_id, schedule, arbor.sampling_policy.exact)
+i_handle = sim.sample(recipe.iprobe_id, schedule, arbor.sampling_policy.exact)
+c_handle = sim.sample(recipe.cprobe_id, schedule, arbor.sampling_policy.exact)
+
+# run simulation for 500 ms of simulated activity and collect results.
+sim.run(tfinal=500)
+
+# extract time, V_m and I_m for each compartment
+V_m_samples, V_m_meta = sim.samples(v_handle)[0]
+I_m_samples, I_m_meta = sim.samples(i_handle)[0]
+I_c_samples, I_c_meta = sim.samples(c_handle)[0]
+
+
+# drop recorded V_m values and corresponding meta data of
+# zero-sized CVs (branch-point potentials)
+inds = np.array([m.dist != m.prox for m in V_m_meta])
+V_m_samples = V_m_samples[:, np.r_[True, inds]]
+V_m_meta = np.array(V_m_meta)[inds].tolist()
+
+
+# note: the cables comprising the metadata for each probe
+# should be the same, as well as the reported sample times.
+assert V_m_meta == I_m_meta
+assert (V_m_samples[:, 0] == I_m_samples[:, 0]).all()
+
+# prep recorded data for plotting
+time = V_m_samples[:, 0]
+V_m = V_m_samples[:, 1:].T
+I_m = I_m_samples[:, 1:].T
+I_c = I_c_samples[:, 1:].T
+
+
+# gather geometry of CVs and assign segments to each CV
+p = arbor.place_pwlin(morphology)
+x, y, z, d = [np.array([], dtype=float).reshape((0, 2))] * 4
+CV_ind = np.array([], dtype=int)  # tracks which CV owns segment
+for i, m in enumerate(I_m_meta):
+    segs = p.segments([m])
+    for j, seg in enumerate(segs):
+        x = np.row_stack([x, [seg.prox.x, seg.dist.x]])
+        y = np.row_stack([y, [seg.prox.y, seg.dist.y]])
+        z = np.row_stack([z, [seg.prox.z, seg.dist.z]])
+        d = np.row_stack([d, [seg.prox.radius * 2, seg.dist.radius * 2]])
+        CV_ind = np.r_[CV_ind, i]
+
+
+###############################################################################
+# compute extracellular potential using segment information
+###############################################################################
+cell_geometry = lfpykit.CellGeometry(
+    x=x,
+    y=y,
+    z=z,
+    d=d
+)
+
+# membrane voltages, transmemrbane current and corresponding times
+cell_geometry.V_m = V_m  # mV
+# nA, sum stimulation and transmembrane current to mimic sinusoid synapse
+cell_geometry.I_m = I_m + I_c
+cell_geometry.time = time  # ms
+
+# locations where extracellular potential is predicted
+dx = 1
+dz = 1
+axis = np.round([x.min() - 10, x.max() + 10, y.min() - 10, y.max() + 10])
+# axis = np.round(axis)
+X, Y = np.meshgrid(np.linspace(axis[0], axis[1], int(np.diff(axis[:2]) // dx) + 1),
+                   np.linspace(axis[2], axis[3], int(np.diff(axis[2:]) // dz) + 1))
+Z = np.zeros_like(X)
+
+# LineSourcePotential object, get mapping for all segments per CV
+lsp = lfpykit.LineSourcePotential(cell=cell_geometry,
+                                  x=X.flatten(),
+                                  y=Y.flatten(),
+                                  z=Z.flatten())
+M_tmp = lsp.get_transformation_matrix()
+
+# Define response matrix from M with columns weighted by area of each frusta
+M = np.zeros((lsp.x.size, I_m.shape[0]))
+for i in range(I_m.shape[0]):
+    inds = CV_ind == i
+    M[:, i] = M_tmp[:, inds] @ (cell_geometry.area[inds] /
+                                cell_geometry.area[inds].sum())
+
+# Extracellular potential using segment information at last time step
+# in x,y-plane coordinates
+V_e = M @ cell_geometry.I_m[:, -1]
+
+
+# ## Plotting
+# Plot the morphology and extracellular potential prediction
+def create_polygon(x, y, d):
+    """create a outline for each segment defined by 1D arrays `x`, `y`, `d`
+    in x,y-plane which can be drawn using `plt.Polygon`
+
+    Parameters
+    ----------
+    x: ndarray
+    y: ndarray
+    d: ndarray
+
+    Returns
+    -------
+    x, y: nested list
+    """
+    # calculate angles
+    dx = abs(np.diff(x))
+    dy = np.diff(y)
+    theta = np.arctan2(dy, dx)
+
+    x = np.r_[x, x[::-1]]
+    y = np.r_[y, y[::-1]]
+
+    theta = np.r_[theta, theta[::-1]]
+    d = np.r_[d, d[::-1]]
+
+    # 1st corner:
+    x[0] -= 0.5 * d[0] * np.sin(theta[0])
+    y[0] += 0.5 * d[0] * np.cos(theta[0])
+
+    # points between start and end of section, first side
+    x[1:dx.size] -= 0.25 * d[1:dx.size] * (
+        np.sin(theta[:dx.size - 1]) + np.sin(theta[1:dx.size]))
+    y[1:dy.size] += 0.25 * d[1:dy.size] * (
+        np.cos(theta[:dy.size - 1]) + np.cos(theta[1:dx.size]))
+
+    # end of section, first side
+    x[dx.size] -= 0.5 * d[dx.size] * np.sin(theta[dx.size])
+    y[dy.size] += 0.5 * d[dy.size] * np.cos(theta[dy.size])
+
+    # end of section, second side
+    x[dx.size + 1] += 0.5 * d[dx.size + 1] * np.sin(theta[dx.size])
+    y[dy.size + 1] -= 0.5 * d[dy.size + 1] * np.cos(theta[dy.size])
+
+    # points between start and end of section, second side
+    x[::-1][1:dx.size] += 0.25 * d[::-1][1:dx.size] * (
+        np.sin(theta[::-1][:dx.size - 1]) + np.sin(theta[::-1][1:dx.size]))
+    y[::-1][1:dy.size] -= 0.25 * d[::-1][1:dy.size] * (
+        np.cos(theta[::-1][:dy.size - 1]) + np.cos(theta[::-1][1:dx.size]))
+
+    # last corner:
+    x[-1] += 0.5 * d[-1] * np.sin(theta[-1])
+    y[-1] -= 0.5 * d[-1] * np.cos(theta[-1])
+
+    return list(zip(x, y))
+
+
+def colorbar(fig, ax, im,
+             width=0.01,
+             height=1.0,
+             hoffset=0.01,
+             voffset=0.0,
+             orientation='vertical'):
+    '''
+    draw matplotlib colorbar without resizing the axes object
+    '''
+    rect = np.array(ax.get_position().bounds)
+    rect = np.array(ax.get_position().bounds)
+    caxrect = [0] * 4
+    caxrect[0] = rect[0] + rect[2] + hoffset * rect[2]
+    caxrect[1] = rect[1] + voffset * rect[3]
+    caxrect[2] = rect[2] * width
+    caxrect[3] = rect[3] * height
+    cax = fig.add_axes(caxrect)
+    cb = fig.colorbar(im, cax=cax, orientation=orientation)
+    return cb
+
+
+fig, ax = plt.subplots(1, 1, figsize=(12, 4))
+
+# plot pcolormesh plot of V_e
+im_V_e = ax.pcolormesh(X, Y, V_e.reshape(X.shape),
+                       shading='auto', cmap='RdBu',
+                       vmin=-abs(V_e).max() / 2, vmax=abs(V_e).max() / 2,
+                       zorder=0)
+cb = colorbar(fig, ax, im_V_e, height=0.45, voffset=0.55)
+cb.set_label('$V_e$ (mV)')
+
+# add outline of each CV
+norm = plt.Normalize(vmin=-66, vmax=-64)
+colors = [plt.cm.viridis(norm(v)) for v in cell_geometry.V_m[:, -1]]
+zips = []
+for i in range(I_m.shape[0]):
+    inds = CV_ind == i
+    zips.append(create_polygon(x[inds, ].flatten(),
+                y[inds, ].flatten(), d[inds, ].flatten()))
+polycol = PolyCollection(zips,
+                         edgecolors='k',
+                         facecolors=colors,
+                         linewidths=0.5,
+                         zorder=2)
+im_V_m = ax.add_collection(polycol)
+
+cb2 = colorbar(fig, ax, im_V_m, height=0.45)
+cb2.set_ticks([0, 0.5, 1])
+cb2.set_ticklabels([-66, -65, -64])
+cb2.set_label(r'$V_m$ (mV)')
+
+ax.set_xlim(X.min(), X.max())
+ax.set_ylim(Y.min(), Y.max())
+ax.set_xlabel(r'$x$ ($\mu$m)')
+ax.set_ylabel(r'$y$ ($\mu$m)')
+
+fig.savefig('single_cell_extracellular_potentials.svg', bbox_inches='tight')
+
+# ## Notes on output:
+# The spatial discretization is here deliberately coarse with only 3 CVs per branch.
+# Hence the branch receiving input about 1/6 of the way from its root
+# (from `decor.place('(location 4 0.16667)', iclamp)`) is treated
+# as 3 separate line sources with homogeneous current density per length unit each, even if
+# the diameter and direction varies with position due to the fact
+# that each CV may  be composed of multiple segments.
+#
+# The parameter `nseg = 3` above can be changed above to affect the number
+# of CVs per branch.
diff --git a/scripts/run_python_examples.sh b/scripts/run_python_examples.sh
index 5eca21dc..2527a55a 100755
--- a/scripts/run_python_examples.sh
+++ b/scripts/run_python_examples.sh
@@ -10,6 +10,8 @@ fi
 
 PREFIX=${1:-}
 
+$PREFIX python -m pip install -r python/example/example_requirements.txt
+
 $PREFIX python python/example/network_ring.py
 $PREFIX python python/example/single_cell_model.py
 $PREFIX python python/example/single_cell_recipe.py
@@ -18,4 +20,5 @@ $PREFIX python python/example/brunel.py -n 400 -m 100 -e 20 -p 0.1 -w 1.2 -d 1 -
 $PREFIX python python/example/single_cell_swc.py python/example/single_cell_detailed.swc
 $PREFIX python python/example/single_cell_detailed.py python/example/single_cell_detailed.swc
 $PREFIX python python/example/single_cell_detailed_recipe.py python/example/single_cell_detailed.swc
+$PREFIX python python/example/single_cell_extracellular_potentials.py python/example/single_cell_detailed.swc
 $PREFIX python python/example/single_cell_cable.py
-- 
GitLab