Model: Hodgkin-Huxley Point Cell
The classic 4-variable conductance-based model:
\[C\frac{dv}{dt} = -g_{Na}\,m^3\,h\,(v - E_{Na}) - g_K\,n^4\,(v - E_K) - g_L\,(v - E_L) + I_{\text{ext}}(t)\]
With gating variable kinetics: \[\frac{dx}{dt} = \alpha_x(v)(1-x) - \beta_x(v)\,x \quad \text{for } x \in \{m, h, n\}\]
The NeuroML2 example uses a pointCellCondBased with separate ion channel populations. By annotating the TVBO dynamics with iri: neuroml:pointCellCondBased and structuring the ion channels as components, TVBO emits standard NeuroML2 types that preserve the jLEMS child-before-parent RK4 evaluation order — giving exact numerical identity with the reference simulation.
1. Define in TVBO
from tvbo import SimulationExperiment
# HH parameters matching NeuroML2 Ex1 (hhpointcell)
# Uses `iri: neuroml:*` annotations to emit standard NeuroML types.
# Parameters with `description: "nml:..."` carry NeuroML-formatted values.
exp = SimulationExperiment.from_string("""
label: "NeuroML Ex1: Hodgkin-Huxley"
dynamics:
name: HodgkinHuxley
iri: neuroml:pointCellCondBased
description: >
Hodgkin-Huxley model matching NeuroML2 Ex1.
Uses standard NeuroML types via hierarchical components.
parameters:
C: { value: 10, description: "nml:10pF" }
v0: { value: -65, description: "nml:-65mV" }
thresh: { value: 20, description: "nml:20mV" }
pulse_delay: { value: 50, description: "nml:50ms" }
pulse_duration: { value: 50, description: "nml:50ms" }
I_amp: { value: 0.08, description: "nml:0.08 nA" }
components:
passive:
name: passive
iri: neuroml:ionChannelPassive
parameters:
conductance: { value: 10, description: "nml:10pS" }
number: { value: 300 }
erev: { value: -54.3, description: "nml:-54.3mV" }
na:
name: na
iri: neuroml:ionChannelHH
parameters:
conductance: { value: 10, description: "nml:10pS" }
number: { value: 120000 }
erev: { value: 50, description: "nml:50mV" }
components:
m:
name: m
iri: neuroml:gateHHrates
parameters:
instances: { value: 3 }
components:
forwardRate:
name: forwardRate
iri: neuroml:HHExpLinearRate
parameters:
rate: { value: 1, description: "nml:1per_ms" }
midpoint: { value: -40, description: "nml:-40mV" }
scale: { value: 10, description: "nml:10mV" }
reverseRate:
name: reverseRate
iri: neuroml:HHExpRate
parameters:
rate: { value: 4, description: "nml:4per_ms" }
midpoint: { value: -65, description: "nml:-65mV" }
scale: { value: -18, description: "nml:-18mV" }
h:
name: h
iri: neuroml:gateHHrates
parameters:
instances: { value: 1 }
components:
forwardRate:
name: forwardRate
iri: neuroml:HHExpRate
parameters:
rate: { value: 0.07, description: "nml:0.07per_ms" }
midpoint: { value: -65, description: "nml:-65mV" }
scale: { value: -20, description: "nml:-20mV" }
reverseRate:
name: reverseRate
iri: neuroml:HHSigmoidRate
parameters:
rate: { value: 1, description: "nml:1per_ms" }
midpoint: { value: -35, description: "nml:-35mV" }
scale: { value: 10, description: "nml:10mV" }
k:
name: k
iri: neuroml:ionChannelHH
parameters:
conductance: { value: 10, description: "nml:10pS" }
number: { value: 36000 }
erev: { value: -77, description: "nml:-77mV" }
components:
n:
name: n
iri: neuroml:gateHHrates
parameters:
instances: { value: 4 }
components:
forwardRate:
name: forwardRate
iri: neuroml:HHExpLinearRate
parameters:
rate: { value: 0.1, description: "nml:0.1per_ms" }
midpoint: { value: -55, description: "nml:-55mV" }
scale: { value: 10, description: "nml:10mV" }
reverseRate:
name: reverseRate
iri: neuroml:HHExpRate
parameters:
rate: { value: 0.125, description: "nml:0.125per_ms" }
midpoint: { value: -65, description: "nml:-65mV" }
scale: { value: -80, description: "nml:-80mV" }
integration:
step_size: 0.01
duration: 150.0
time_scale: ms
""" )
print (f"Model: { exp. dynamics. name} " )
print (f"IRI: { exp. dynamics. iri} " )
print (f"Components: { list (exp.dynamics.components.keys())} " )
Model: HodgkinHuxley
IRI: neuroml:pointCellCondBased
Components: ['k', 'na', 'passive']
This example uses iri: neuroml:pointCellCondBased with nested components for ion channels (ionChannelHH, ionChannelPassive) and gates (gateHHrates). TVBO’s adapter detects these annotations and emits standard NeuroML2 XML elements rather than custom ComponentType definitions. This preserves the hierarchical RK4 evaluation order in jLEMS, giving exact numerical identity with the reference simulation at the original step size (0.01 ms) — no gate_rate_scale hack needed.
2. Render LEMS XML
xml = exp.render("lems" )
# Show key elements of the generated XML
for line in xml.split(' \n ' ):
line_stripped = line.strip()
if any (tag in line_stripped for tag in ['<ionChannel' , '<gateHH' , '<pointCell' ,
'<channelPopulation' , '<pulseGenerator' , 'Rate type=' , '<Include' ]):
print (line_stripped)
<Include file="Cells.xml"/>
<Include file="Networks.xml"/>
<Include file="Simulation.xml"/>
<ionChannelHH id="k" conductance="10pS">
<gateHHrates id="n" instances="4">
<forwardRate type="HHExpLinearRate" rate="0.1per_ms" midpoint="-55mV" scale="10mV"/>
<reverseRate type="HHExpRate" rate="0.125per_ms" midpoint="-65mV" scale="-80mV"/>
<ionChannelHH id="na" conductance="10pS">
<gateHHrates id="h" instances="1">
<forwardRate type="HHExpRate" rate="0.07per_ms" midpoint="-65mV" scale="-20mV"/>
<reverseRate type="HHSigmoidRate" rate="1per_ms" midpoint="-35mV" scale="10mV"/>
<gateHHrates id="m" instances="3">
<forwardRate type="HHExpLinearRate" rate="1per_ms" midpoint="-40mV" scale="10mV"/>
<reverseRate type="HHExpRate" rate="4per_ms" midpoint="-65mV" scale="-18mV"/>
<ionChannelPassive id="passive" conductance="10pS"/>
<pointCellCondBased id="HodgkinHuxley" C="10pF" v0="-65mV" thresh="20mV">
<channelPopulation id="k_pop" ionChannel="k" number="36000" erev="-77mV"/>
<channelPopulation id="na_pop" ionChannel="na" number="120000" erev="50mV"/>
<channelPopulation id="passive_pop" ionChannel="passive" number="300" erev="-54.3mV"/>
<pulseGenerator id="pulseGen1" delay="50ms" duration="50ms" amplitude="0.08 nA"/>
3. Run Reference
import sys, os
sys.path.insert(0 , os.path.dirname(os.path.abspath("." )))
from _nml_helpers import run_lems_example
ref_outputs = run_lems_example("LEMS_NML2_Ex1_HH.xml" )
for name, arr in ref_outputs.items():
print (f" { name} : shape= { arr. shape} , t=[ { arr[0 ,0 ]:.4f} , { arr[- 1 ,0 ]:.4f} ]" )
hh_v.dat: shape=(15001, 2), t=[0.0000, 0.1500]
4. Run TVBO Version
import numpy as np
result = exp.run("neuroml" )
da = result.integration.data
time = da.coords['time' ].values
# Standard NeuroML output has only voltage
v_data = da.sel(variable= 'v' ).values
tvbo_arr = np.column_stack([time, v_data])
print (f"TVBO: shape= { tvbo_arr. shape} , t=[ { tvbo_arr[0 ,0 ]:.4f} , { tvbo_arr[- 1 ,0 ]:.4f} ]" )
TVBO: shape=(15001, 2), t=[0.0000, 0.1500]
5. Numerical Comparison
from _nml_helpers import compare_traces
import numpy as np
ref_arr = list (ref_outputs.values())[0 ]
# Compare voltage traces
ref_v = ref_arr[:, [0 , 1 ]]
tvbo_v = tvbo_arr[:, [0 , 1 ]]
compare_traces(ref_v, tvbo_v, ref_cols= ['time' , 'v' ], tvbo_cols= ['time' , 'v' ])
v: RMSE=0.000000 max_err=0.000000 corr=1.000000 ✅
{'v': {'rmse': np.float64(5.069514854437176e-21),
'max_err': np.float64(4.336808689942018e-19),
'corr': np.float64(1.0),
'close': True}}
6. Plot
from _nml_helpers import plot_comparison
ref_v = ref_arr[:, [0 , 1 ]]
tvbo_v = tvbo_arr[:, [0 , 1 ]]
plot_comparison(
ref_v, tvbo_v,
ref_cols= ['time' , 'v' ], tvbo_cols= ['time' , 'v' ],
title= "Ex1: Hodgkin-Huxley — NeuroML vs TVBO" ,
time_scale= 1.0 , time_unit= "s" ,
)