Channel models (Notebook)¶
In this tutorial, we will learn how to work with ion channel models.
Setup¶
!pip install dendrotweaks --quiet
If you are using Google Colab, you might also need to restart the session as the installation downgraded some packages (numpy). You can do it manually or programmatically as shown below:
# import os
# os.kill(os.getpid(), 9)
import numpy as np
import matplotlib.pyplot as plt
import os
Let’s begin by importing the standard libraries and the dendrotweaks library.
import dendrotweaks as dd
dd.__version__
'0.4.2'
dd.apply_dark_theme() # Set the theme for the plots
os.makedirs('examples', exist_ok=True)
if not os.listdir('examples/'):
print("Downloading example data...")
dd.download_example_data('examples')
path_to_model = os.path.join('examples', 'Toy')
print(f'Path to model: {path_to_model}')
model = dd.Model(path_to_model)
Path to model: examples/Toy
Converting MOD to Python¶
The MODFileConverter
class conveniently encapsulates the process of converting MOD files into Python code. Let’s create an instance of this class.
converter = dd.biophys.io.MODFileConverter()
converter.reader, converter.parser, converter.generator
(<dendrotweaks.biophys.io.reader.MODFileReader at 0x7f2c74cd5350>,
<dendrotweaks.biophys.io.parser.MODFileParser at 0x7f2c76b68e50>,
<dendrotweaks.biophys.io.code_generators.PythonCodeGenerator at 0x7f2ca300cd50>)
path_to_mod_file = os.path.join('examples', 'Toy', 'biophys', 'mod', 'Kv.mod')
path_to_python = os.path.join('examples', 'Toy','biophys', 'python', 'stdKv.py')
path_to_template = os.path.join('examples', 'Templates', 'default.py')
converter.convert(path_to_mod_file, path_to_python, path_to_template)
Saved content to examples/Toy/biophys/python/stdKv.py
The MODReader
class reads the files, performs basic preprocessing steps like removing comments and empty lines, and splits the file content into distinct blocks.
blocks = converter.reader.get_blocks()
Split content into blocks:
1 - TITLE
1 - COMMENT
1 - NEURON
1 - UNITS
1 - PARAMETER
1 - ASSIGNED
1 - STATE
1 - BREAKPOINT
1 - DERIVATIVE
1 - INITIAL
1 - FUNCTION
1 - PROCEDURE
0 - KINETIC
blocks
{'TITLE': ['TITLE Kv_Park_ref\n'],
'COMMENT': ['COMMENT\n26 Ago 2002 Modification of original channel to allow variable time step and to correct an initialization error.\n Done by Michael Hines(michael.hines@yale.e) and Ruggero Scorcioni(rscorcio@gmu.edu) at EU Advance Course in Computational Neuroscience. Obidos, Portugal\n\nkv.mod\n\nPotassium channel, Hodgkin-Huxley style kinetics\nKinetic rates based roughly on Sah et al. and Hamill et al. (1991)\n\nAuthor\nENDCOMMENT'],
'NEURON': ['NEURON {\n\tSUFFIX Kv\n\tUSEION k READ ek WRITE ik\n\tRANGE gbar, i, v12, q\n}'],
'UNITS': ['UNITS {\n\t(mA) = (milliamp)\n\t(mV) = (millivolt)\n\t(S) = (siemens)\n\t(um) = (micron)\n}'],
'PARAMETER': ['PARAMETER {\n\tgbar = 0.0 (S/cm2) \n\tRa = 0.02 (/mV/ms) \n\tRb = 0.006 (/mV/ms) \n\tv12 = 25 (mV)\t \n\tq = 9 (mV) \n\ttemp = 23 (degC) \n\tq10 = 2.3 (1)\t \n}'],
'ASSIGNED': ['ASSIGNED {\n\tv (mV)\n\ti \t (mA/cm2)\n\tik (mA/cm2)\n\tgk (S/cm2)\n\tek (mV)\n\tninf (1)\n\tntau (ms)\n\ttadj (1)\n\tcelsius (degC)\n}'],
'STATE': ['STATE { n }'],
'BREAKPOINT': ['BREAKPOINT {\n SOLVE states METHOD cnexp\n\tgk = tadj * gbar * n\n\ti = gk * (v - ek)\n\tik = i\n}'],
'DERIVATIVE': ["DERIVATIVE states { \n rates(v)\n n' = (ninf - n)/ntau\n}"],
'INITIAL': ['INITIAL { \n\trates(v)\n\tn = ninf\n}'],
'FUNCTION': ['FUNCTION rateconst(v (mV), r (/mV/ms), th (mV), q (mV)) (/ms) {\n\trateconst = r * (v - th) / (1 - exp(-(v - th)/q))\n}'],
'PROCEDURE': ['PROCEDURE rates(v (mV)) {\n\n\tLOCAL alpn, betn\n\n\ttadj = q10^((celsius - temp)/10(degC))\n\n\talpn = rateconst(v, Ra, v12, q)\n\tbetn = rateconst(v, -Rb, v12, -q)\n\n ntau = 1 / (tadj * (alpn + betn))\n\tninf = alpn/(alpn + betn)\n}'],
'KINETIC': []}
The MOD files are parsed using the PyParsing library. The grammar used for parsing can be found in the dendrotweaks/biophys/grammar.py
file. The parser generates an Abstract Syntax Tree (AST) from the MOD file. The AST is a hierarchical representation of the MOD file that can be used to generate Python code.
ast = converter.parser.get_ast()
ast
{'TITLE': ' Kv_Park_ref',
'COMMENT': '\n'
'26 Ago 2002 Modification of original channel to allow variable '
'time step and to correct an initialization error.\n'
' Done by Michael Hines(michael.hines@yale.e) and Ruggero '
'Scorcioni(rscorcio@gmu.edu) at EU Advance Course in Computational '
'Neuroscience. Obidos, Portugal\n'
'\n'
'kv.mod\n'
'\n'
'Potassium channel, Hodgkin-Huxley style kinetics\n'
'Kinetic rates based roughly on Sah et al. and Hamill et al. '
'(1991)\n'
'\n'
'Author\n',
'NEURON': {'suffix': 'Kv',
'useion': [{'ion': 'k', 'read': ['ek'], 'write': ['ik']}],
'range': ['gbar', 'i', 'v12', 'q']},
'UNITS': {'mA': 'milliamp', 'mV': 'millivolt', 'S': 'siemens', 'um': 'micron'},
'PARAMETER': [{'name': 'gbar', 'value': 0.0, 'unit': 'S/cm2'},
{'name': 'Ra', 'value': 0.02, 'unit': '/mV/ms'},
{'name': 'Rb', 'value': 0.006, 'unit': '/mV/ms'},
{'name': 'v12', 'value': 25, 'unit': 'mV'},
{'name': 'q', 'value': 9, 'unit': 'mV'},
{'name': 'temp', 'value': 23, 'unit': 'degC'},
{'name': 'q10', 'value': 2.3, 'unit': '1'}],
'ASSIGNED': [{'name': 'v', 'unit': 'mV'},
{'name': 'i', 'unit': 'mA/cm2'},
{'name': 'ik', 'unit': 'mA/cm2'},
{'name': 'gk', 'unit': 'S/cm2'},
{'name': 'ek', 'unit': 'mV'},
{'name': 'ninf', 'unit': '1'},
{'name': 'ntau', 'unit': 'ms'},
{'name': 'tadj', 'unit': '1'},
{'name': 'celsius', 'unit': 'degC'}],
'STATE': {'n': {'power': 1}},
'BREAKPOINT': {'solve_stmt': {'solve': 'states', 'method': 'cnexp'},
'statements': [{'assigned_var': 'gk',
'expression': '(tadj * gbar) * n'},
{'assigned_var': 'i',
'expression': 'gk * (v - ek)'},
{'assigned_var': 'ik', 'expression': 'i'}]},
'DERIVATIVE': {'name': 'states',
'func_calls': [{'rates': ['v']}],
'statements': [{'assigned_var': 'n',
'expression': '(ninf - n) / ntau'}]},
'INITIAL': {'statements': [{'rates': ['v']},
{'assigned_var': 'n', 'expression': 'ninf'}]},
'FUNCTION': [{'signature': {'name': 'rateconst',
'params': [{'name': 'v', 'unit': 'mV'},
{'name': 'r', 'unit': '/mV/ms'},
{'name': 'th', 'unit': 'mV'},
{'name': 'q', 'unit': 'mV'}],
'returned_unit': '/ms'},
'locals': [],
'statements': [{'assigned_var': 'rateconst',
'expression': '(r * (v - th)) / (1 - '
'np.exp((-(v - th) / q)))'}]}],
'PROCEDURE': [{'signature': {'name': 'compute_kinetic_variables',
'params': [{'name': 'v', 'unit': 'mV'}]},
'locals': ['alpn', 'betn'],
'statements': [{'assigned_var': 'tadj',
'expression': 'q10 ** ((celsius - temp) / 10)'},
{'assigned_var': 'alpn',
'expression': 'rateconst(v, Ra, v12, q)'},
{'assigned_var': 'betn',
'expression': 'rateconst(v, -Rb, v12, -q)'},
{'assigned_var': 'nTau',
'expression': '1 / (tadj * (alpn + betn))'},
{'assigned_var': 'nInf',
'expression': 'alpn / (alpn + betn)'}]}]}
The PythonCodeGenerator
class generates Python code from the AST using a JINJA template file.
print(converter.generator.content)
# This Python channel class was automatically generated from a MOD file
# using DendroTweaks toolbox, dendrotweaks.dendrites.gr
import sys
from dendrotweaks.biophys.mechanisms import IonChannel
import numpy as np
class Kv(IonChannel):
"""
Kv_Park_ref
"""
def __init__(self, name="Kv"):
super().__init__(name=name)
self.params = {
"gbar": 0.0,
"Ra": 0.02,
"Rb": 0.006,
"v12": 25,
"q": 9,
"temp": 23,
"q10": 2.3
}
self.range_params = {
"gbar": 0.0,
"v12": 25,
"q": 9
}
self.states = {
"n": 0.0
}
self._state_powers = {
"n": {'power': 1}
}
self.ion = "k"
self.current_name = "i_k"
self.current_available = True
self.independent_var_name = "v"
self.temperature = 37
def __getitem__(self, item):
return self.params[item]
def __setitem__(self, item, value):
self.params[item] = value
def compute_kinetic_variables(self, v):
Ra = self.params["Ra"]
Rb = self.params["Rb"]
v12 = self.params["v12"]
q = self.params["q"]
alpn = self.rateconst(v, Ra, v12, q)
betn = self.rateconst(v, -Rb, v12, -q)
nTau = 1 / (self.tadj * (alpn + betn))
nInf = alpn / (alpn + betn)
return nInf, nTau
def rateconst(self, v, r, th, q):
rateconst = (r * (v - th)) / (1 - np.exp((-(v - th) / q)))
return rateconst
Working with channels¶
Let’s create a simple point neuron model and add channels to it.
path_to_model = os.path.join('examples', 'Toy')
print(f'Path to model: {path_to_model}')
Path to model: examples/Toy
model = dd.Model(path_to_model)
model.list_morphologies()
['ball-and-stick', 'point', 'simple-cell']
model.load_morphology(file_name='simple-cell')
Sorted PointTree(root=Point(idx=0), num_nodes=17).
Apical dendrite is already aligned.
Extended 6 nodes.
Sorted PointTree(root=Point(idx=0), num_nodes=23).
model.sec_tree.plot(
show_points=True,
annotate=True,
)

On the model level, we can create an ion channel using the add_mechanism
method. First, let’s add the default mechanisms to the model, which include the leak channel, calcium dynamics and synaptic mechanisms.
Next, we add the sodium and potassium channels to the model.
model.add_default_mechanisms()
model.add_mechanism('Na')
model.add_mechanism('Kv')
Saved content to examples/Toy/biophys/python/Na.py
Saved content to examples/Toy/biophys/python/Kv.py
With these commands, we create Python objects from MOD files and add them to mechanisms. We also compile and load the MOD files, making them available in NEURON.
Once the ion channel is created, its kinetics can be visualized using the plot_kinetics
method.
fig, ax = plt.subplots(1, 2, figsize=(10, 5))
model.mechanisms['Kv'].plot_kinetics(ax=ax)
Got data for v in range -100.0 to 100.0 at 37°C

Each mechanism object is an instance of the Mechanism
class, which contains information about the mechanism, such as its name and parameters. To examine the parameters of a specific mechanism, we can use the params
attribute:
model.mechanisms['Kv'].params
{'gbar': 0.0,
'Ra': 0.02,
'Rb': 0.006,
'v12': 25,
'q': 9,
'temp': 23,
'q10': 2.3}
After uploading the mechanisms in the previous step, we now need to insert them into specific domains. In this example, we insert each of the three available mechanisms to all domains.
for domain_name in model.domains:
for mech_name in model.mechanisms:
model.insert_mechanism(mech_name, domain_name, distribute=False)
At this point, we have inserted the mechanisms into the membrane and set the default parameters for the model. We can inspect the parameters of the inserted mechanisms using the df_params
or params
attribute.
model.df_params[model.df_params['Mechanism'] == 'Kv']
Mechanism | Parameter | Group | Distribution | Distribution params | |
---|---|---|---|---|---|
24 | Kv | gbar_Kv | all | constant | {'value': 0.0} |
25 | Kv | v12_Kv | all | constant | {'value': 25} |
26 | Kv | q_Kv | all | constant | {'value': 9} |
Let’s update channel conductances and run a simulation in out point neuron.
model.set_param('gbar_Leak', value=0.00015)
model.set_param('gbar_Na', value=0.03)
model.set_param('gbar_Na', group_name='somatic', value=0.05)
model.set_param('gbar_Kv', value=0.003)
model.set_param('gbar_Kv', group_name='somatic', value=0.005)
model.remove_all_stimuli()
model.remove_all_recordings()
soma = model.sec_tree[0]
model.add_recording(soma, loc=0.5)
model.add_iclamp(soma, loc=0.5, amp=0.1, delay=50, dur=200)
Recording added to sec NeuronSection(idx=0) at loc 0.5.
IClamp added to sec NeuronSection(idx=0) at loc 0.5.
model.run(300)
fig, ax = plt.subplots(1, 1, figsize=(10, 4))
model.simulator.plot_voltage(ax=ax)
t, v_orig = model.simulator.t, model.simulator.recordings['v'][soma(0.5)]

Standardizing ion channel models¶
DendroTweaks provides a simple way to standardize voltage-gated ion channel models.
Assuming that we have an instance of a model with a custom sodium channel Na created before, we can standardize it with the model.standardize_channel
method. The standard channel with the name stdNa
(standard-Na) and very similar kinetics to the original channel model is created to replace the original channel model. This method also exports the standard channel model to a MOD file which is immediately loaded into the NEURON simulator.
fig, ax = plt.subplots(1, 2, figsize=(10, 5))
model.mechanisms['Na'].plot_kinetics(ax=ax)
model.standardize_channel('Na');
model.mechanisms['stdNa'].plot_kinetics(ax=ax, linestyle='--')
Got data for v in range -100.0 to 100.0 at 37°C
Got data for v in range -100.0 to 100.0 at 23°C
WARNING: Mechanism Na is not inserted in any domain and will be removed.
(model.py, line 897)
Saved content to examples/Toy/biophys/mod/stdNa.mod
Got data for v in range -100.0 to 100.0 at 37°C

Let’s do the same for the potassium channel:
fig, ax = plt.subplots(1, 2, figsize=(10, 5))
model.mechanisms['Kv'].plot_kinetics(ax=ax)
model.standardize_channel('Kv');
model.mechanisms['stdKv'].plot_kinetics(ax=ax, linestyle='--')
Got data for v in range -100.0 to 100.0 at 37°C
Got data for v in range -100.0 to 100.0 at 23°C
WARNING: Mechanism Kv is not inserted in any domain and will be removed.
(model.py, line 897)
Saved content to examples/Toy/biophys/mod/stdKv.mod
Got data for v in range -100.0 to 100.0 at 37°C

We can plot the voltage trace of the standardized model to compare it with the trace produced by the original model.
model.run(300)
fig, ax = plt.subplots(1, 1, figsize=(10, 4))
ax.plot(t, v_orig, color='C0', linestyle='-', zorder=0, lw=1, label='original')
model.simulator.plot_voltage(ax=ax, linestyle='--', color='C1')
