Note: This file also exists in Markdown format in this repo, generated
at an unspecified point in time with
`pandoc -f org -t gfm -o nml.md nml.org`.
# Introduction
This document describes my current knowledge of NeuroML2 and in a way
aims to replace its missing developer documentation. It is likely flawed
and constantly evolving. However, this text is needed as the actual
projects' documentation is sparse and sometimes confusing and, most
importantly, thin on the high concepts.
NeuroML2 is an effort to provide portable descriptions of
neuro-scientific simulations, including networks, and morphologically
detailled neurons. NeuroML2 supersedes NeuroML1 and is not a simple
iteration. Therefore, NeuroML will be used synonymously with NeuroML2
and abbreviated as NML or NML2.
Despite its flaws, NeuroML2 is a valiant effort and an important feature
to support.
# NeuroML2 and LEMS
**TL;DR**: NeuroML2 is a library of definitions written in LEMS, which
is written in XML. All NeuroML2 is LEMS.
Here I lay out the overall look and feel of NeuroML2, links to in-depth
sections follow.
NeuroML2 is – in contrast to v1 – implemented in LEMS[^1], modelling
language for chemical, physical, and biological processes. Furthermore,
NML2 has a kind of symbiotic relationship with LEMS in the large, which
will be explored in depth later.
Both NML2 and LEMS are written in XML, making the human interaction
slightly awkward. Schemata (XSD) exist in the relevant git repositories.
These form the basis of our exploration of NML2.
The fundamental relationship between LEMS and NML2 is this: every
functional item in NML2 has a matching definition in LEMS. So, to
interpret a NML2 file, we need to look up these definitions in the
corresponding LEMS files. An example might look like this
- LEMS definition
``` xml
```
- NML2 file
``` xml
```
The tag `` introduces a new type, which can be
instanced by using `` or the shorthand
``, the latter being the idiomatic use, especially in NML2. For
brevity, I skipped the definition of units and dimensions. Because of
this model, no discussion (or implementation) can proceed without
constantly switching between LEMS and NML2.
Both LEMS and NML2 follow an inheritance model akin to object-oriented
programming. This means
- A `ComponentType` may specify its 'base class' using
`extends="base"`.
- Whenever a value of type `base` is required, a derived type may be
substituted.
- *Thankfully* we only have to deal with single inheritance
Again an example
- LEMS definition
``` xml
```
- NML2
``` xml
```
There is also the concept of public and private data members. In
particular access is mediated by 'exposures', which declare externally
readable members.
LEMS:
``` xml
```
Here we also introduced the tags `Parameter` (externally settable value)
and `DerivedVariable` (dependent quantity). `Parameter` values are set
using XML attributes. The internal variable `q10_fix` is only readable
through the exposure `q10`, even in enclosing scopes.
NML2:
``` xml
```
Again we encounter two new bits of related functionality: paths and
select. The rules are as follows:
If a `ComponentType` declares a `Child` of type `type` with an exposed
value `name`, that value can be used via `select` by addressing the
*path* `type/name`. `Child` implies a single instance of type `type` to
be used. However, multiple instances can be part of a `ComponentType`,
which is declared using `Children`. Addressing values from children is
done slightly differently. A *single* child's value is addressed using
`select="child_id/name"`. It is possible to accumulate over children
using `select="children[*]/name
reduce="multiply"` or `select="children[*]/name reduce="add"`. More
complicated selection is possible, see later.
Example, assume the definitions from above being available
LEMS:
``` xml
```
NML2:
``` xml
```
Here, `prod_q10=64` and `first_q10=1`, although is general there is no
way of knowing `fixed_1` being present, unless enforced otherwise.
# Inheritance
As alluded to above, the LEMS/NML2 model for describing relations
between components is similar to the 'is-a' type inheritance model used
in Python or C++. To illustrate, we use the HH model from the NML2
'stdlib found in `Channels.xml`
``` xml
```
Note the following consequences of the 'is-a' model
- derived items have access to their bases' `Parameter` values.
- types can have a `Requirement` on the presence of certain variable
in the surrounding scope.
- `HHExpRate` can be inserted instead of a `baseHHRate` **or**
`baseVoltageDepRate`.
**QUESTION** from the examples and discussions I have the impression
that having a `Dynamics` item in both base and derived items will cause
the item from base to overwritten. Is this correct and where is it
documented? **ANSWER** Correct, but nowhere written.
# Recap So Far
As we have learned in the sections above, NML2 is *written in* LEMS,
where the salient definitions can be found in the LEMS files provided
within the NeuroML2 repositories. This means each XML tag in an NML
document, eg ``, can be looked up in the corresponding
LEMS file, in this case `Channels.xml`.
In addition, both LEMS and NML2 have – minimal, ie a working NML2/LEMS
document will likely validate, but not every validating document is
working NML2/LEMS – XSD schemata attached.[^2]
To compose a concrete component from an NML2 document we will be
required to
- parse the related LEMS files
- build an inheritance tree (since requirements are in terms of 'base
classes')
- extract dynamics and other items from the LEMS descriptions
- obtain parameters and similar from the NML2 XML node
- recursively instantiate children of the object
From there, we can interpret the structure and integrate the equations
of motion or produce an optimised represe first and interpret the result
or export it to another format like native code or NMODL.
# A Larger Example
We are almost in a position to decode our first practical example
``` xml
```
To do so, we have to find the LEMS definitions, of which will we use
simplified versions.
First, we take a look at the ion channel.
``` xml
```
Recall the definitions for the `rate` hierarchy above. Finally, we need
to inspect the `gates`
``` xml
```
Finally, we can put this together to extract the meaning of
`gateHHrates`
where *a* and *b* are defined by the `forwardRate` and `reverseRate`
items. Now, we can compose this into `ionChannelHH` while simplifying
the equations and renaming quantities
NB. while we *are* using the same symbols here, *r**e**x**p*
signifies two independent quantities in the equations for *m* and *h*.
As notational convenience, we used greek letters for parameters.
Unsurprisingly, we find the HH neuron model here.
# How to Interpret NML2
In the last part we saw that *intuitively* NML2/LEMS is quite
straightforward to interpret. However, we will need to formalise the
process to be able to deal with NML2 in general. Starting with this
section, the document will delve into the technical details. Our current
goal will be to write a tool able to translate at least the
`ionChannelHH` model into a symbolic description. Although NML2 can be
extended using LEMS, we will use the static schema provided in the NML2
repository as is.
Here is a list of invariants we need to uphold
- Each type must be aware of its base type, eg `gateHHrates <- gate`,
required to
- check and sort `Children`, eg given
`` we need to collect
everything deriving from `gate` into an array `gates`.
- compose the derived items parameters and variables from the
inherited items
- we cannot shorten `A <- B <- C` to `A <- C` since `C` might be
used as either
- Each item must be aware of its enclosing items
``` example
NaConductance - gates +- m +- forwardRate
| +- reverseRate
|
+- h +- forwardRate
+- reverseRate
```
This is needed to facilitate path-based selection.
We need to situationally account for these cases
- `A` declares a ``
``` xml
```
or
``` xml
```
**QUESTION** is this acceptable as well?
``` xml
```
- `A` declares a ``
``` xml
```
The name `children` now relates to a collection of things deriving
from `B`, ie `\forall T \sup B: String -> T`, akin to dynamic
polymorphism/existentials.
**NOTE** the dichotomy between `` and ``
for `Child` and `Children` is a bit annoying.
The possible instantiations of `ionChannel` are
`baseIonChannel`
the base class
`ionChannel` / `ionChannelHH`
the are identical, but both present for convenience
`ionChannelKS`
ion channel with a `gate` based on a kinetic scheme
`ionChannelVShift`
ion channel with a voltage offset.
Practically, we only expect to support `ionChannel` for now and
voltage shifted channels being a trivial extension later. Kinetic
schemata are out of scope and `baseIonChannel` is of no practical
use.
However, it infeasible to generate a fixed set of prebuilt
implementations as `ionChannel` instances can have an arbitrary
number of `gate` instances which implement most of the
functionality. This invalidates previous plans for implementation
following that strategy.
**QUESTION** Is the `id` field always allowed? **ANSWER** Yes, `id`
is implied.
## Scoping
- derived variables:
- local: visible
- enclosing: exposure
- enclosed: exposure
- state variables:
- local: visible
- enclosing: exposure
- enclosed: exposure
- parameters
- local: visible
- enclosing: private
- enclosed: private
- constants:
- local: visible
- enclosing: private
- enclosed: private
## Flattening Instances
In order to produce efficient representations, we will need to collapse
instances and types into a flat format, eg
``` xml
```
should be flattened into
``` xml
```
# Synapses
Next, we consider synapses, which have a different hierarchy, which is
reproduced in the following, again, simplified for ease of reading [^3]
``` xml
```
There's some additions to our knowledge of NML2 here:
`Property`
Paraphrasing the LEMS documentation: 'like `Parameter`, but may be
different for each instance'. So far, we did not encounter the
`Instance` concept in LEMS and as it is used here for the synapse
weight, we are going to largely ignore it. [^4]
`OnEvent`
Contains assignments to state variables, similar to `OnStart`, but now
triggered upon incoming events, ie spikes. May choose a port.
Finally, we need to discuss `EventPorts`. In contrast to simulators like
Arbor and Neuron NML2 can differentiate between sources of incoming
events and selectively emit events to channels. In pratice, however,
only three such ports seem to be used in NML2 [^5]+"' \| sort -u\`\]
in
Receive spikes
relay
Forward spikes to sub-components
spike
Emit spikes
### Implementation Details
ports
Of these we only need to support `in` and `relay`, the former is
covered, the latter currently not.
weight
As Arbor uses this construction for NMODL input
``` example
net_receive(weight) { ... }
```
we need to use our prior knowledge about the semantics of the parameter
and hard-code it into our NMODL translator.
vpeer
Similar to `weight`, sometimes needed for `gradedSynapse`, can be
modelled in Arbor's NMODL dialect.
# Gap Junctions
Gap junctions are similar to synapses and have the following hierarchy
\
\ \ \ \
\
\ \ \ \ \
The issue here with our scheme is that neither `peer` nor `vpeer` exist
in our implementation of NML2. However, Arbor's NMODL dialect exposes
`v_peer` as a global property.
# NMODL Export
While we have hinted at NMODL and Arbor a few times so far, our
implementation of NML2 has been almost completely generic. To export to
NMODL though, we need to bridge a significant gap. After we have
processed the instantiations into either an instance or a collapsed
instance, we edit the resulting model based on special cases.
At the moment, let us focus on current based models only, ie all our
instances need to produce an ionic transmembrane current, dubbed `iX` in
NMODL. Later, we might extend this to concentration models. For synapses
and gap junctions this is defined directly. Ion channels produce only a
conductance value, `g`, so we add a derived variable `iX =g (v - eX)`
where `X` is replaced by the ionic species.
Based on the respective sections on implementation details, we edit the
syntax trees to eliminate explicit definitions of `vpeer` and similar
values, replacing them with Arbor's built-in variables.
We then build individual blocks of the NMODL language from the AST by
inspecting dependencies of the state variables and building the
appropriate chains of expressions. All intermediates are stored as
`LOCAL` to minimise memory accesses.
The style defined by NML2, especially the
# Kinetic Schemes
Kinetic schemes describe ion channels by a set of discrete states and
transition probabilities between those states. Again we reproduce a
simplified version of the `ionChannelKS` hierarchy.
``` xml
```
Apart from the type of `gates` this is functionally identical to
`ionChannelHH`. Now we delve into the definitions of `gateKS`
``` xml
```
Again, not much is new here, apart from `KineticScheme`, a list of
states is given in `states` and their transition rates in `transitions`.
Next, we look into the description of these transitons
``` xml
```
Again not much of a surprise here, but we do need to figure out what
`Link` means. The final component is the linked state
``` xml
```
Now, let us consider a simple example from the NML2 sources.
``` xml
```
we can interpret this as
- define two populations transitioning between states open `o1` and
closed `c1`
- call these fractions `o1_occupancy` and `c1_occupancy`
- conserve `o1_occupancy + c1_occupancy = 1` for all times
- transition `c1 -> o1` with rate `ft/rate/r`
- transition `o1 -> c1` with rate `rt/rate/r`
- calculate
`ft/rate/r`
as prescribed by `HHExpLinearRate`
`rt/rate/r`
as prescribed by `HHExpRate`
In NMODL we would like to generate a `KINETIC` block like this
``` example
KINETIC scheme {
: ... snip ...
gates_n_states_o1_to_c1 = gates_n_transitions_ft_rr + gates_n_transitions_rt_rr
gates_n_states_c1_to_o1 = gates_n_transitions_ft_rf + gates_n_transitions_rt_rf
gates_m_states_o1_to_c1 = gates_m_transitions_ft_rr + gates_m_transitions_rt_rr
gates_m_states_c1_to_o1 = gates_m_transitions_ft_rf + gates_m_transitions_rt_rf
~ gates_n_states_o1_occupancy <-> gates_n_states_c1_occupancy (gates_n_states_o1_to_c1, gates_n_states_c1_to_o1)
~ gates_m_states_o1_occupancy <-> gates_m_states_c1_occupancy (gates_m_states_o1_to_c1, gates_m_states_c1_to_o1)
}
```
where multiple rates between the same two states were merged. Note that
- `gates_n_transitions_ft_rr = 0`
- `gates_n_transitions_ft_rf = 0`
- `gates_m_transitions_ft_rr = 0`
- `gates_m_transitions_ft_rf = 0`
## On `KineticScheme`
`KineticScheme` seems to break the abstractions put in place. Looking at
`nodes` and `edges` which must be of kind `children`, otherwise
the semantics would not work out. Similarly, both must be located
directly under the scheme in the hierarchy. However, a `select`
statement would have expressed the same thing more idiomatically.
# The Final Building Block
So far, we have dealt exclusively with channels, but NML2 requires some
more levels above this for completely specilying the dynamics
Cell
defines dynamics of membrane potential in terms of currents (mediated by
channels)
BioPhys Properties
specifies capacitance, resistivities.
Membrane Properties
initial potential, channels.
Channel Density
gives the area density of a channel.
In order to interact with cable models like Arbor or Neuron, we need to
pull these layers into the channel descriptions – partially at least. In
the end we would like to automatically compose Arbor simulations from
NML2 files, but for now, we have to extract those values manually.
Working our way up
``` xml
```
As noted under *NMODL Export* we compute `iX = g(E - U)`
where `g` is the `ionChannel`'s conductance, see above
`g = fopen * conductance`. The parameter `condDensity` added in
`channelDensityCond` is identical to `conductance`. Thus, we need to
retain `conductance` and set it to the value of `condDensity`.
# Much Time Has Passed
# Networks
Networks are important for us for both actual network support and
simulation inputs. We note the following about the network layout in
NML2.
Instances for input and synapse appear at top-level, next to cells and
networks. Thus any `.nml` file is eligible to define them.
Networks are defined in terms of populations and
projections.
## Populations
Populations can be written in two ways inside a `network`
``
`n` duplicates of `X`; using `MultiInstantiate` which seems to have no
particular definition.
``
give children as list of `` with a 3D position, all have type
`X`.
For us, there seems to be no practical difference between them. In both
cases `X` names a `ComponentType` deriving `baseCell`.
## Projections
Projections specify pre- and post-synaptic populations and a connection
type, eg a synapse. Then, a list of connections is given as tuples
`(pre: location, post:
location)` where `location` takes the form
`(cell: id, segment: id, fraction:
[0, 1])`
Target cells are selected by the relative paths and an index, eg
`target=../pop0[0]` addresses the population `pop0`, one tag up from
here, and picks cell zero from there. If `pop0` has a single member, the
index is optional. Alternatively it seems (?) we can address using
`pop//Cell`
### Questions
- Does `[ix]` query by id or offset?
- What's the difference between `pop/id/Cell` and `/pop[0]`?
- Why do connections have to traverse the tree and do not work
relative to their pre/post populations? Seems counterintuitive.
- How does Arbor translate segments into its internal rep? Can we
directly use it like `branch`?
### Example
``` xml
```
## Inputs
- inputs are similar to (one-sided) connections
- targets are written as
- id, default = 0
- fraction, default = 0.5
- stimuli derive `basePointCurrent`
- pulse, sine, cosine
- relevant tags:
`explicitInput`
`inputList`
### Example
``` xml
```
[^1]: Low-entropy modelling language. Using the moniker 'low-entropy'
for an XML based format must be an attempt at humor.
[^2]: For example the use of `sequence` in XSD implies order, but I am
pretty sure this is not upheld everywhere. Also, most often
`sequence` is used together with `count=unbounded`, which is
semantically incorrect in many cases
[^3]: Note the comment, which we ignore is if it was fixed and fix in
our own code.
[^4]: Arbor adds this on the library side to connections.
[^5]: Confirmed via \`rg -Io 'port="\[^"