Note
Click here to download the full example code
Extract Neuron Parameters¶
This example demonstrates how to extract electrophysiological parameters from MyoGen motor neurons using the Fuglevand recruitment model.
Parameters Extracted:
- Vhold: Resting membrane potential (soma)
- Rin: Input resistance
- tau: Membrane time constant
- Ir: Rheobase (minimum current for action potential)
- AP: Action potential amplitude
- AHP: Afterhyperpolarization depth
- AHPdur: Full AHP duration
- FI_gain: Frequency-current gain (slope of F-I curve)
Usage:
# Extract from default 5 neurons
python 09_extract_neuron_parameters.py
# Extract from custom number of neurons
python 09_extract_neuron_parameters.py --n-neurons 10
Note: For advanced features (parallel processing, model comparison, slope scan),
see sandbox/neuron_parameter_extraction/README.md
Import Libraries¶
import logging
import os
os.environ["MPLBACKEND"] = "Agg"
if "DISPLAY" in os.environ:
del os.environ["DISPLAY"]
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from neuron import h
import myogen
from myogen import simulator
from myogen.simulator.neuron.populations import AlphaMN__Pool
from myogen.utils.nmodl import load_nmodl_mechanisms
# Simple plotting style
plt.style.use("seaborn-v0_8-darkgrid")
sns.set_context("paper", font_scale=1.2)
# Setup NEURON environment
h.load_file("stdrun.hoc")
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
Core Parameter Extraction Functions¶
def fi0_multicompartment(sections: list, voltages: list) -> None:
"""Initialize voltages for multiple compartments."""
for sec, v in zip(sections, voltages):
sec.v = v
def get_vhold__mV(cell, sections: list, voltages: list, tstop__ms: float = 500.0) -> float:
"""
Measure resting membrane potential for soma.
Returns
-------
float
Steady-state voltage in soma (mV).
"""
h.tstop = tstop__ms
h.dt = 0.0125
h.celsius = 36
_ = h.FInitializeHandler(0, lambda: fi0_multicompartment(sections, voltages))
vsoma = h.Vector()
vsoma.record(cell.soma(0.5)._ref_v)
h.finitialize()
h.run()
return vsoma.to_python()[-1]
def get_rin__MOhm(
cell,
vhold__mV: float,
sections: list,
amplitudes__nA: list[float] = None,
) -> float:
"""
Measure input resistance using current steps.
Returns
-------
float
Input resistance in MegaOhms.
"""
if amplitudes__nA is None:
amplitudes__nA = [-5.0, -4.0, -3.0, -2.0, -1.0]
h.tstop = 1000.0
h.dt = 0.0125
h.celsius = 36
voltages = [vhold__mV] * len(sections)
_ = h.FInitializeHandler(0, lambda: fi0_multicompartment(sections, voltages))
vsoma = h.Vector()
vsoma.record(cell.soma(0.5)._ref_v)
delta_v__mV = []
for amp__nA in amplitudes__nA:
stim = h.IClamp(cell.soma(0.5))
stim.amp = amp__nA
stim.dur = 1000.0
stim.delay = 0
h.finitialize()
h.run()
vpeak__mV = vsoma.to_python()[-1]
dv = vpeak__mV - vhold__mV
delta_v__mV.append(dv)
del stim
coefficients = np.polyfit(amplitudes__nA, delta_v__mV, 1)
rin__MOhm = coefficients[0]
return rin__MOhm
def get_time_constant__ms(cell, vhold__mV: float, sections: list) -> float:
"""
Measure membrane time constant from exponential decay.
Returns
-------
float
Membrane time constant in milliseconds.
"""
h.tstop = 200.0
h.dt = 0.0125
h.celsius = 36
voltages = [vhold__mV] * len(sections)
_ = h.FInitializeHandler(0, lambda: fi0_multicompartment(sections, voltages))
vsoma = h.Vector()
t = h.Vector()
vsoma.record(cell.soma(0.5)._ref_v)
t.record(h._ref_t)
stim = h.IClamp(cell.soma(0.5))
stim.amp = -20.0 # nA
stim.dur = 1.0 # ms
stim.delay = 5.0 # ms
h.finitialize()
h.run()
vsoma_array = vsoma.as_numpy()
t_array = t.as_numpy()
v_min__mV = np.min(vsoma_array)
v_min_index = np.where(vsoma_array == v_min__mV)[0][0]
recovery_indices = np.where(vsoma_array[v_min_index:] > vhold__mV - 0.1)[0]
if len(recovery_indices) > 0:
v_end_index = recovery_indices[0] + v_min_index
else:
v_end_index = len(vsoma_array) - 1
t_fit = t_array[v_min_index:v_end_index]
v_fit = vsoma_array[v_min_index:v_end_index]
log_v_fit = np.log(np.abs(v_fit - vhold__mV))
coefficients = np.polyfit(t_fit, log_v_fit, 1)
tau__ms = -1.0 / coefficients[0]
return tau__ms
def get_rheobase__nA(
cell, vhold__mV: float, sections: list, max_iterations: int = 500
) -> float:
"""
Find minimum current required to elicit an action potential.
Returns
-------
float
Rheobase current in nanoamperes, or np.nan if no spike found.
"""
h.tstop = 100.0
h.dt = 0.0125
h.celsius = 36
voltages = [vhold__mV] * len(sections)
_ = h.FInitializeHandler(0, lambda: fi0_multicompartment(sections, voltages))
vsoma = h.Vector()
vsoma.record(cell.soma(0.5)._ref_v)
stim = h.IClamp(cell.soma(0.5))
stim.dur = 50.0
stim.delay = 0
spike_threshold__mV = vhold__mV + 40.0
amp__nA = 0.1
iteration = 0
while iteration < max_iterations:
stim.amp = amp__nA
h.finitialize()
h.run()
vpeak__mV = np.max(vsoma.as_numpy())
if vpeak__mV >= spike_threshold__mV:
return amp__nA
amp__nA += 0.1
iteration += 1
logger.warning(f"No spike found after {max_iterations} iterations")
return np.nan
def get_ap_and_ahp(cell, vhold__mV: float, sections: list) -> dict:
"""
Measure action potential and afterhyperpolarization characteristics.
Returns
-------
dict
Contains 'AP__mV', 'AHP__mV', 'AHPdur__ms'
"""
h.tstop = 900.0
h.dt = 0.0125
h.celsius = 36
voltages = [vhold__mV] * len(sections)
_ = h.FInitializeHandler(0, lambda: fi0_multicompartment(sections, voltages))
vsoma = h.Vector()
t = h.Vector()
vsoma.record(cell.soma(0.5)._ref_v)
t.record(h._ref_t)
stim = h.IClamp(cell.soma(0.5))
stim.dur = 0.5
stim.delay = 5.0
amp__nA = 35.0
vpeak__mV = vhold__mV
spike_threshold__mV = vhold__mV + 40.0
while vpeak__mV < spike_threshold__mV:
amp__nA += 10.0
stim.amp = amp__nA
h.finitialize()
h.run()
vsoma_array = vsoma.as_numpy()
vpeak__mV = np.max(vsoma_array)
vvalley__mV = np.min(vsoma_array)
if amp__nA > 500.0:
logger.warning("Current exceeded 500 nA without spike")
return {"AP__mV": np.nan, "AHP__mV": np.nan, "AHPdur__ms": np.nan}
ap__mV = vpeak__mV - vhold__mV
ahp__mV = vhold__mV - vvalley__mV
t_array = t.as_numpy()
peak_index = np.where(vsoma_array == vpeak__mV)[0][0]
valley_index = np.where(vsoma_array == vvalley__mV)[0][0]
recovery_indices = np.where(vsoma_array[valley_index:] > vhold__mV - 0.15)[0]
if len(recovery_indices) > 0:
recovery_index = recovery_indices[0] + valley_index
ahp_dur__ms = t_array[recovery_index] - t_array[peak_index]
else:
ahp_dur__ms = np.nan
return {"AP__mV": ap__mV, "AHP__mV": ahp__mV, "AHPdur__ms": ahp_dur__ms}
def get_fi_gain(cell, vhold__mV: float, sections: list, rheobase__nA: float) -> float:
"""
Measure frequency-current (F-I) gain.
Returns
-------
float
F-I gain in Hz/nA.
"""
if np.isnan(rheobase__nA):
return np.nan
h.tstop = 3000.0
h.dt = 0.0125
h.celsius = 36
voltages = [vhold__mV] * len(sections)
_ = h.FInitializeHandler(0, lambda: fi0_multicompartment(sections, voltages))
stim = h.IClamp(cell.soma(0.5))
stim.dur = 3000.0
stim.delay = 0
current_levels = np.arange(5.0, 30.5, 5.0)
firing_rates = []
for current__nA in current_levels:
stim.amp = current__nA
apc = h.APCount(cell.soma(0.5))
apc.thresh = 50
h.finitialize()
h.run()
spike_count = apc.n
firing_rate__Hz = (spike_count / 2500.0) * 1000.0
firing_rates.append(firing_rate__Hz)
firing_rates = np.array(firing_rates)
fit_mask = current_levels >= rheobase__nA
if np.sum(fit_mask) >= 2:
fit_currents = current_levels[fit_mask]
fit_rates = firing_rates[fit_mask]
coefficients = np.polyfit(fit_currents, fit_rates, 1)
gain__Hz_per_nA = coefficients[0]
return gain__Hz_per_nA
else:
return np.nan
def extract_all_parameters(cell, cell_index: int, recruitment_threshold: float) -> dict:
"""
Extract all electrophysiological parameters from a single neuron.
Returns
-------
dict
Dictionary containing all extracted parameters and metadata.
"""
logger.info(f"Extracting parameters for cell {cell_index}")
sections = [cell.soma] + cell.dend
v_init__mV = -67.0
voltages = [v_init__mV] * len(sections)
result = {"cell_index": cell_index, "recruitment_threshold": recruitment_threshold}
# Extract parameters
try:
vhold__mV = get_vhold__mV(cell, sections, voltages)
result["vhold_soma__mV"] = vhold__mV
except Exception as e:
logger.error(f"Cell {cell_index}: Failed to get Vhold - {e}")
result["vhold_soma__mV"] = np.nan
vhold__mV = v_init__mV
try:
result["Rin__MOhm"] = get_rin__MOhm(cell, vhold__mV, sections)
except Exception as e:
logger.error(f"Cell {cell_index}: Failed to get Rin - {e}")
result["Rin__MOhm"] = np.nan
try:
result["tau__ms"] = get_time_constant__ms(cell, vhold__mV, sections)
except Exception as e:
logger.error(f"Cell {cell_index}: Failed to get tau - {e}")
result["tau__ms"] = np.nan
try:
ir__nA = get_rheobase__nA(cell, vhold__mV, sections)
result["Ir__nA"] = ir__nA
except Exception as e:
logger.error(f"Cell {cell_index}: Failed to get rheobase - {e}")
result["Ir__nA"] = np.nan
try:
ap_ahp_params = get_ap_and_ahp(cell, vhold__mV, sections)
result.update(ap_ahp_params)
except Exception as e:
logger.error(f"Cell {cell_index}: Failed to get AP/AHP - {e}")
result.update({"AP__mV": np.nan, "AHP__mV": np.nan, "AHPdur__ms": np.nan})
try:
rheobase__nA = result.get("Ir__nA", np.nan)
result["FI_gain__Hz_per_nA"] = get_fi_gain(cell, vhold__mV, sections, rheobase__nA)
except Exception as e:
logger.error(f"Cell {cell_index}: Failed to get F-I gain - {e}")
result["FI_gain__Hz_per_nA"] = np.nan
logger.info(f"Cell {cell_index}: Extraction complete")
return result
Visualization Functions¶
def visualize_results(df: pd.DataFrame, save_path: Path) -> None:
"""Create basic visualization plots of extracted parameters."""
logger.info("Creating visualizations")
df_valid = df.dropna(subset=["Ir__nA"])
if len(df_valid) == 0:
logger.warning("No valid data for visualization")
return
x = df_valid["cell_index"] + 1
# Figure 1: Key parameters
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Rheobase
axes[0, 0].scatter(x, df_valid["Ir__nA"], s=100, alpha=0.7)
axes[0, 0].set_ylabel("Rheobase (nA)")
axes[0, 0].set_title("Rheobase Current")
axes[0, 0].grid(True, alpha=0.3)
# Input Resistance
axes[0, 1].scatter(x, df_valid["Rin__MOhm"], s=100, alpha=0.7, color="C1")
axes[0, 1].set_ylabel("Input Resistance (MΩ)")
axes[0, 1].set_title("Input Resistance")
axes[0, 1].grid(True, alpha=0.3)
# AHP Duration
axes[1, 0].scatter(x, df_valid["AHPdur__ms"], s=100, alpha=0.7, color="C2")
axes[1, 0].set_xlabel("Motor Unit #")
axes[1, 0].set_ylabel("AHP Duration (ms)")
axes[1, 0].set_title("AHP Duration")
axes[1, 0].grid(True, alpha=0.3)
# F-I Gain
axes[1, 1].scatter(x, df_valid["FI_gain__Hz_per_nA"], s=100, alpha=0.7, color="C3")
axes[1, 1].set_xlabel("Motor Unit #")
axes[1, 1].set_ylabel("F-I Gain (Hz/nA)")
axes[1, 1].set_title("F-I Gain")
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
fig_path = save_path / "neuron_parameters.png"
plt.savefig(fig_path, dpi=300, bbox_inches="tight")
logger.info(f"Saved figure to {fig_path}")
plt.show()
# Figure 2: Rin vs Tau
fig2, ax = plt.subplots(figsize=(8, 6))
scatter = ax.scatter(
df_valid["Rin__MOhm"],
df_valid["tau__ms"],
c=df_valid["cell_index"],
cmap="viridis",
s=100,
alpha=0.7,
)
cbar = plt.colorbar(scatter, ax=ax)
cbar.set_label("Motor Unit Index")
ax.set_xlabel("Input Resistance (MΩ)")
ax.set_ylabel("Membrane Time Constant (ms)")
ax.set_title("Input Resistance vs Time Constant")
ax.grid(True, alpha=0.3)
plt.tight_layout()
fig2_path = save_path / "rin_tau_relationship.png"
plt.savefig(fig2_path, dpi=300, bbox_inches="tight")
logger.info(f"Saved figure to {fig2_path}")
plt.show()
Main Function¶
def main():
"""Main demonstration function."""
import argparse
parser = argparse.ArgumentParser(
description="Extract electrophysiological parameters from MyoGen motor neurons"
)
parser.add_argument(
"--n-neurons",
type=int,
default=5,
help="Number of neurons to extract (default: 5)",
)
parser.add_argument(
"--seed",
type=int,
default=42,
help="Random seed for reproducibility (default: 42)",
)
args = parser.parse_args()
# Setup
save_path = Path("./results")
save_path.mkdir(exist_ok=True)
myogen.set_random_seed(args.seed)
logger.info(f"Random seed set to {args.seed}")
load_nmodl_mechanisms()
logger.info("MyoGen NEURON mechanisms loaded")
# Generate recruitment thresholds using Fuglevand model
logger.info(f"Generating {args.n_neurons} neurons with Fuglevand model")
recruitment_thresholds, _ = simulator.RecruitmentThresholds(
N=args.n_neurons,
recruitment_range__ratio=100,
mode="fuglevand",
)
# Create motor neuron pool
logger.info("Creating motor neuron pool")
pool = AlphaMN__Pool(recruitment_thresholds__array=recruitment_thresholds)
logger.info(f"Created pool with {len(pool._cells)} neurons")
# Extract parameters from all neurons
logger.info(f"Extracting parameters from {args.n_neurons} neurons")
results = []
for i in range(args.n_neurons):
result = extract_all_parameters(pool[i], i, recruitment_thresholds[i])
results.append(result)
# Convert to DataFrame and save
df = pd.DataFrame(results)
csv_path = save_path / "neuron_parameters.csv"
df.to_csv(csv_path, index=False)
logger.info(f"Saved results to {csv_path}")
# Visualize
visualize_results(df, save_path)
logger.info("Parameter extraction complete!")
logger.info("\nSummary statistics:")
logger.info(f" Rheobase range: {df['Ir__nA'].min():.2f} - {df['Ir__nA'].max():.2f} nA")
logger.info(f" Rin range: {df['Rin__MOhm'].min():.2f} - {df['Rin__MOhm'].max():.2f} MΩ")
logger.info(f" F-I gain range: {df['FI_gain__Hz_per_nA'].min():.2f} - {df['FI_gain__Hz_per_nA'].max():.2f} Hz/nA")
if __name__ == "__main__":
main()

Out:
Total running time of the script: ( 0 minutes 47.676 seconds)
Download Python source code: 10_extract_neuron_parameters.py