============================
Multicompartmental modelling
============================

.. warning::

    The extensions to the API for multicompartmental models are an experimental feature.
    We encourage users to try them out, but at this stage we do not recommend publishing
    work using these extensions without very thorough checking and validation of results.


Cell types
==========

As noted in :doc:`neurons`, the "standard library" approach described above needs to be modified for morphologically and biophysically-detailed neuron models,
as the number of possible combinations of morphologies, ion-channel models and synapse models is infinite.

Rather than a library of standard neuron models, we have a library of standard components (ion-channel models, synapse models)
from which multi-compartment models can be composed by writing a simple class:

.. code-block:: python

    class MyNeuronModel(MultiCompartmentNeuron):
        ion_channels = {
            'pas': sim.PassiveLeak,  # standard library of ion channel models
            'na': sim.NaChannel,
            'kdr': sim.KdrChannel
        }
        post_synaptic_entities = {
            'AMPA': sim.CondExpPostSynapticResponse,  # standard library of synapse models
            'GABA_A': sim.CondExpPostSynapticResponse
        }

.. note:: here we are using ion channel and synapse models from a standard library, but we also plan to support
          LEMS (NeuroML) and NineML model specifications for this.


Parameterization
================

When it come to instantiating such a cell type in order to create a population, three extensions are needed when compared
to the point-neuron case:

**Hierarchical parameter sets**
    Ion channel and synapse parameters should be in a separate dictionary nested inside the main parameter dictionary.

**More complex parameter types**
    For the standard library of point neurons, most of the parameters are floats, with :class:`Sequence` being the only special case.
    For morphologically-detailed neurons, one of the parameters is a representation of an entire morphology, since
    different neurons within a population can be expected to have different morphologies.

**Spatial parameterization within neuron morphologies**
    Point neurons may have parameter values that depend on the position of the neuron in space.
    Neurons with axons and dendrites may have parameters whose value depends on location *within* a single neuron.


Handling of morphologies and spatial parameterization are described in the next section.


Morphologies
============

A number of existing Python tools for handling neuronal morphologies already exist
(e.g. libNeuroML_, `Brian 2`_, MorphIO_, btmorph_)
In the interest of integrating with other open-source simulation tools and standards wherever possible,
rather than reinventing the wheel (see :doc:`mc_aims`), we would like to make use of these tools within PyNN,
and give users a choice of tools where there is overlap.

Our proposed implementation of this is to define an interface through a parent :class:`Morphology`,
which can be sub-classed for the different Python representations of neuronal morphology,
e.g. :class:`NeuroMLMorphology`, :class:`BrianMorphology`.

This standard interface for working with morphologies allows us to define tools for spatial parameterization,
and, more generally, for specifying points and regions within the neurites, in a format-independent way.

The general principle is to present an interface to the user that treats the neuron as a continuous
tree structure, and hides details of the spatial discretization, while mapping points and regions defined by the user
to the particular section/segment used internally by the simulator.


Selecting points and regions
----------------------------

For simple morphologies, each section/segment/compartment of the neuron can be given an explicit label.
Where we wish to distinguish a point *within* a section, we can add a `fraction_along` parameter.
By default, a label is taken to refer to the centre of a section.

Thus, for example, if we specify a two-compartment neuron using NeuroML:

.. code-block:: python

    from neuroml import Segment, Point3DWithDiam as P
    from pyNN.morphology import NeuroMLMorphology

    soma = Segment(proximal=P(x=0, y=0, z=0, diameter=18.8),
                   distal=P(x=18.8, y=0, z=0, diameter=18.8),
                   name="soma")
    dend = Segment(proximal=P(x=0, y=0, z=0, diameter=2),
                   distal=P(x=-500, y=0, z=0, diameter=2),
                   name="dendrite",
                   parent=soma)

    cell_type = cell_class(morphology=NeuroMLMorphology(segments=(soma, dend)),
                           ...other parameters...)
    cells = sim.Population(n, cell_type)


Then we can, for example, specify where to inject a current using the segment labels:

.. code-block:: python

    step_current = sim.DCSource(amplitude=0.1, start=50.0, stop=150.0)
    step_current.inject_into(cells[0:1], location="soma")
    step_current.inject_into(cells[1:2], location="dendrite")

or, similarly, where to record the membrane voltage:

.. code-block:: python

    cells.record('v', locations=['soma', 'dendrite'])


For detailed morphologies it is less easy/useful to address/label specific sections,
with the exception of the soma and perhaps the axon initial segment.
We create such a morphology using, for example:

.. code-block:: python

    from pyNN.morphology import load_morphology

    pyr_morph = load_morphology("oi15rpy4-1.CNG.swc")

    class PyramidalCell(MultiCompartmentNeuron):
        ion_channels = {
            'pas': sim.PassiveLeak,
            'na': sim.NaChannel,
            'kdr': sim.KdrChannel
        }
        ...

    pyramidal_cell_type = PyramidalCell(morphology=pyr_morph,
                                        ...other parameters...)
    pyramidal_cells = sim.Population(n, pyramidal_cell_type)


We can select regions of the axonal and/or dendritic trees using a morphology filter.
At present, the available filters are :class:`soma`, :class:`apical_dendrites`, :class:`basal_dendrites`,
:class:`dendrites`, :class:`axon`, and :class:`all`.

.. note:: identifying apical and basal dendrites depends on the morphology description file providing these labels.
          This is the case for SWC files, and it is also supported by NeuroML.


Within these regions, we can specify parameters whose values depend on the position within the region,
for example, distance along the dendrites from the soma:

.. code-block:: python

    kdr_conductance_density_apical = by_distance(apical_dendrites(), lambda d: 0.05 * d / 200.0)
    kdr_conductance_density_basal = uniform(basal_dendrites(), lambda d: 0.07)


To specify specific points within a region (for injecting current, placing synapses or recording state variables)
we specify a "location generator":

.. code-block:: python

    locations = at_distances(apical_dendrites(), [50, 100, 150, 200, 250])  # distances in µm

Note that this may specify more than five points, due to branching
(i.e. there will be a point on each branch at 100 µm from the soma);
equally, some branches may be shorter than 250 µm and so no point will be generated for that branch.

In addition to :class:`at_distances`, PyNN currently provides :class:`centre` and :class:`random_placement`;
points can also be defined using labels provided in the morphology definition.
Further location generator methods are planned.


Projections
===========

In making synaptic connections onto neurons with detailed morphologies,
we have to consider not only which post-synaptic neuron to connect to and which synapse *type* to use,
but also in which location within the morphology the synapse should be.

There are two possible strategies when placing the post-synaptic component of synapses on neurons:

1. place the post-synaptic mechanisms when creating the population (parameterized in the cell type),
   then when making connections select from this pre-existing list;
2. place the post-synaptic mechanisms at the moment of creating connections.

Both strategies are in fairly widespread use, so it would be worthwhile to support both approaches in PyNN.

For Strategy 1, we specify the synaptic density when creating the cell type,
as seen in the "pyramidal_cell" example above.
It should also be possible to specify the placement of individual synapses in labelled sections,
in the simple, few-compartment case (todo).

For Strategy 2, post-synaptic parameters are specified in the :class:`Connector` class, see next section.


In making synaptic connections *from* neurons with detailed morphologies,
we need to consider the location of the source of the action potential.
Where the axon is represented explicitly, this location could be any point in the axon (terminal section or en-passant).
Where the axon is not represented explicitly or is truncated, this location will be the soma, action initial segment,
or terminal point of the truncation.
In this case the synaptic delay should include a distance-dependent component, where the distance could be
as-the-crow-flies, or the path length along the original axon (before truncation).
In both cases, this is specified using the `source` argument of the :class:`Projection` constructor.


Connectors
==========

As noted above, for a given type of synapse, we have to consider both which post-synaptic neuron to connect to
and in which location within the morphology the synapse should be.

In principle, these two choices could be independent, or the choice of location could depend on which post-synaptic
neuron was chosen.

To support the second, more general case, we propose that the PyNN :class:`Connector` classes should implement
the algorithms for both choices in a single class. The first case, of independent choices, is likely to be fairly
common however, so we also propose to extend the standard connectors from earlier versions of PyNN with a
`location_selector` argument, so for example we might create a projection with:

.. code-block:: python

    sim.Projection(inputs, pyramidal_cells,
                   connector=sim.AllToAllConnector(location_selector=sample(apical_dendrites())),
                   synapse_type=sim.StaticSynapse(weight=0.5, delay=0.5),
                   receptor_type="AMPA")

This will connect all inputs to all pyramidal cells, selecting one synapse location at random from the apical dendrites
(this random location would be different for each neuron in the pyramidal cell population).

If Strategy 1 (as described above) is being used,
the random sample is over those post-synaptic mechanisms already placed in the apical dendrites.

If Strategy 2 is being used, the random sample is over all possible locations in the apical dendrites,
and the post-synaptic mechanism is inserted at the same time as the connection is created.
In this scenario, it should be possible to set post-synaptic model and parameters at the same time, for example:

.. code-block:: python

        sim.Projection(inputs, pyramidal_cells,
                   connector=sim.AllToAllConnector(location_selector=sample(apical_dendrites())),
                   synapse_type=sim.StaticSynapse(weight=0.5, delay=0.5),
                   receptor_type=sim.CondExpPostSynapticResponse(tau_syn=RandomDistribution('normal', (0.2, 0.01))))



.. _libNeuroML: https://libneuroml.readthedocs.io/en/latest/
.. _`Brian 2`: http://briansimulator.org
.. _NeuroM: http://neurom.readthedocs.io/en/stable/
.. _MorphIO: https://morphio.readthedocs.io/en/latest/
.. _btmorph: https://github.com/btorb/btmorph_v2
