Ex1: Hodgkin-Huxley

Classic Hodgkin-Huxley conductance-based cell with Na, K, and leak channels

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']
Standard NeuroML Types

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",
)