import os, sys
from contextlib import contextmanager
from .exceptions import *
import numpy as np
if not os.getenv('READTHEDOCS'):
from patch import p
from patch.objects import Section
import glia as g
from .synapse import Synapse
p.load_file('stdlib.hoc')
p.load_file('import3d.hoc')
[docs]class Builder:
"""
A builder is a method interface that exposes an ``instantiate`` method that can be
passed the model that is being instantiated. You can create your own builder by
initializing a builder with a builder function, which is a function that takes the
model and all it's contructor arguments as its own arguments. It's a builder's
responsibility to add or label sections.
Constructing your own Builders is of limited use, because every model's
``morphologies`` field takes morphology files and/or builder functions and
automatically constructs and applies the ``Builder`` from there:
.. code-block:: python
class MyNeuron(NeuronModel):
@staticmethod
def build(model, *args, **kwargs):
model.soma.append(p.Section())
model.dendrites.append(p.Section())
model.axon.append(p.Section())
# Creates 2 different morphologies for this cell model.
morphologies = [
build, # Create 1 soma, dendrite & axonal compartment
('morfo2.swc', self.extend_axon) # First loads morfo2.swc
]
"""
def __init__(self, builder):
self.builder = builder
[docs] def instantiate(self, model, *args, **kwargs):
self.builder(model, *args, **kwargs)
[docs]class ComboBuilder(Builder):
"""
Chains together multiple morphology files and/or builder functions.
"""
def __init__(self, *pipeline):
"""
Chain together multiple morphology files and/or builder functions.
:param pipeline: Morphology file strings or builder functions.
:type pipeline: vararg. str/function.
"""
def outer_builder(model, *args, **kwargs):
for part in pipeline:
# Apply all builders in the pipeline sequence in order.
builder = make_builder(part)
builder.instantiate(model, *args, **kwargs)
self.builder = outer_builder
[docs]class NeuronModel:
"""
The base class that helps you describe your model. Generate all the required
sections, insert all mechanisms and define all synapses using the appropriate
class variables. See the :doc:`/neuron_model`
"""
def __init__(self, position=None, morphology_id=0):
# Check if morphologies were specified
if not hasattr(self.__class__, "morphologies") or len(self.__class__.morphologies) == 0:
raise ModelClassError("All NeuronModel classes should specify a non-empty array of morphologies")
# Import the morphologies if they haven't been imported yet
if not hasattr(self.__class__, "imported_morphologies"):
self.__class__._import_morphologies()
# Initialize variables
self.position = np.array(position if not position is None else [0., 0., 0.])
self.dendrites = []
self.axon = []
self.soma = []
morphology_loader = self.__class__.imported_morphologies[morphology_id]
# Use the Import3D/Builder to instantiate this cell.
morphology_loader.instantiate(self)
# Wrap the neuron sections in our own Section, if not done by the Builder
self.soma = [s if isinstance(s, Section) else Section(p, s) for s in self.soma]
self.dend = [s if isinstance(s, Section) else Section(p, s) for s in self.dend]
self.axon = [s if isinstance(s, Section) else Section(p, s) for s in self.axon]
self.dendrites = self.dend + self.dendrites
del self.dend
self.sections = self.soma + self.dendrites + self.axon
for section in self.sections:
section.synapses = []
# Do labelling of sections into special sections
self._apply_labels()
# Initialize the labelled sections
for section in self.sections:
self._init_section(section)
# Call boot method so that child classes can easily do stuff after init.
self.boot()
def __getattr__(self, attribute):
if attribute == "Vm":
raise NotRecordingError("Trying to read Vm of a cell that is not recording." + " Use `.record_soma()` to enable recording of the soma.")
@classmethod
def _import_morphologies(cls):
cls.imported_morphologies = []
for morphology in cls.morphologies:
builder = make_builder(morphology)
cls.imported_morphologies.append(builder)
def _apply_labels(self):
for section in self.sections:
if not hasattr(section, "labels"):
section.labels = []
for section in self.soma:
section.labels.append("soma")
for section in self.dendrites:
section.labels.append("dendrites")
for section in self.axon:
section.labels.append("axon")
# Apply special labels
if hasattr(self.__class__, "labels"):
for label, category in self.__class__.labels.items():
targets = self.__dict__[category["from"]]
if "id" in category:
l = category["id"]
for id, target in enumerate(targets):
if l(id):
target.labels.append(label)
elif "diam" in category:
l = category["diam"]
for id, target in enumerate(targets):
if l(target.diam):
target.labels.append(label)
def _init_section(self, section):
section.cell = self
# Set the amount of sections to some standard odd amount
section.nseg = 1 + (2 * int(section.L / 40))
for label in section.labels:
self._init_section_label(section, label)
def _init_section_label(self, section, label):
# Store a map of mechanisms to full mod_names for the attribute setter
resolved_mechanisms = {}
definition = self.__class__.section_types[label]
# Insert the mechanisms
for mechanism in definition["mechanisms"]:
# Use Glia to resolve the mechanism selection.
if isinstance(mechanism, tuple):
# Mechanism defined as: `(mech_name, mech_variant)`
mechanism_variant = mechanism[1]
mechanism = mechanism[0]
mod_name = g.resolve(mechanism, pkg="dbbs_mod_collection", variant=mechanism_variant)
else:
# Mechanism defined as string
mod_name = g.resolve(mechanism, pkg="dbbs_mod_collection")
# Map the mechanism to the mod name
resolved_mechanisms[mechanism] = mod_name
# Use Glia to insert the resolved mod.
g.insert(section, mod_name)
# Set the attributes on this section and its mechanisms
for attribute, value in definition["attributes"].items():
mechanism_notice = ""
if isinstance(attribute, tuple):
# `attribute` is an attribute of a specific mechanism and defined
# as `(attribute, mechanism)`. This makes use of the fact that
# NEURON provides shorthands to a mechanism's attribute as
# `attribute_mechanism` instead of having to iterate over all
# the segments and setting `mechanism.attribute` for each
mechanism = attribute[1]
mechanism_notice = " specified for '{}'".format(mechanism)
if not mechanism in resolved_mechanisms:
raise MechanismNotPresentError("The attribute " + repr(attribute) + " specifies a mechanism '{}' that was not inserted in this section.".format(mechanism))
mechanism_mod = resolved_mechanisms[mechanism]
attribute_name = attribute[0] + "_" + mechanism_mod
else:
# `attribute` is an attribute of the section and is defined as string
attribute_name = attribute
# Check whether the value is callable, if so, pass it the section diameter
# and update the local variable to the return value. This allows parameters to
# depend on the diameter of the section.
if callable(value):
value = value(section.diam)
# Use setattr to set the obtained attribute information. __dict__
# does not work as NEURON's Python interface is incomplete.
try:
setattr(section.__neuron__(), attribute_name, value)
except AttributeError as e:
raise SectionAttributeError("The attribute '{}'{} is not found on a section labelled '{}' in the {}.".format(
attribute_name,
mechanism_notice,
",".join(section.labels),
self.__class__.__name__
)) from None
# Copy the synapse definitions to this section
if "synapses" in definition:
if not hasattr(section, "available_synapse_types"):
section.available_synapse_types = []
section.available_synapse_types.extend(definition["synapses"].copy())
[docs] def set_reference_id(self, id):
'''
Add an id that can be used as reference for outside software.
'''
self.ref_id = id
[docs] def connect(self, from_cell, from_section, to_section, synapse_type=None):
'''
Connect this cell as the postsynaptic cell in a connection with
`from_cell` between the `from_section` and `to_section`.
Additionally a `synapse_type` can be specified if there's multiple
synapse types present on the postsynaptic section.
:param from_cell: The presynaptic cell.
:type from_cell: :class:`.NeuronModel`
:param from_section: The presynaptic section.
:type from_section: :class:`.Section`
:param to_section: The postsynaptic section.
:type to_section: :class:`.Section`
:param synapse_type: The name of the synapse type.
:type synapse_type: string
'''
synapse = self.create_synapse(to_section, synapse_type=synapse_type)
to_section.synapses.append(synapse)
from_section.connect_points(synapse._point_process)
return synapse
[docs] def record_soma(self):
self.Vm = self.soma[0].record()
return self.Vm
[docs] def create_synapse(self, section, synapse_type=None):
'''
Create a synapse in the specified ``section`` based on the synapse definitions
present on this model. Additionally a `synapse_type` can be specified if
there's multiple synapse types present on the section.
:param section: The postsynaptic section.
:type section: :class:`.Section`
:param synapse_type: The name of the synapse type.
:type synapse_type: string
'''
labels = section.labels
labels_name = ",".join(labels)
if not hasattr(self.__class__, "synapse_types"):
raise ModelClassError("Can't connect to a NeuronModel that does not specify any `synapse_types` on its class.")
synapse_types = self.__class__.synapse_types
if not hasattr(section, "available_synapse_types") or not section.available_synapse_types:
raise ConnectionError("Can't connect to '{}' labelled section without available synapse types.".format(labels_name))
section_synapses = section.available_synapse_types
if synapse_type is None:
if len(section_synapses) != 1:
raise AmbiguousSynapseError("Too many possible synapse types: " + ", ".join(section_synapses) + ". Specify a `synapse_type` for the connection.")
else:
synapse_definition = synapse_types[section_synapses[0]]
else:
if not synapse_type in section_synapses:
raise SynapseNotPresentError("The synapse type '{}' is not present on '{}' labelled section in {}.".format(synapse_type, labels_name, self.__class__.__name__))
elif not synapse_type in synapse_types:
raise SynapseNotDefinedError("The synapse type '{}' is used on '{}' labelled section but not defined in the model.".format(synapse_type, labels_name))
else:
synapse_definition = synapse_types[synapse_type]
synapse_attributes = synapse_definition["attributes"] if "attributes" in synapse_definition else {}
synapse_point_process = synapse_definition["point_process"]
synapse_variant = None
if isinstance(synapse_point_process, tuple):
synapse_variant = synapse_point_process[1]
synapse_point_process = synapse_point_process[0]
return Synapse(self, section, synapse_point_process, synapse_attributes, variant=synapse_variant)
@contextmanager
def _suppress_stdout():
with open(os.devnull, "w") as devnull:
old_stdout = sys.stdout
sys.stdout = devnull
try:
yield
finally:
sys.stdout = old_stdout
def _import3d_load(morphology):
from . import _morphology_dirs
for dir in _morphology_dirs:
file = os.path.join(dir, morphology)
if not os.path.isfile(file):
continue
loader = p.Import3d_Neurolucida3()
with _suppress_stdout():
loader.input(file)
loaded_morphology = p.Import3d_GUI(loader, 0)
return loaded_morphology
raise FileNotFoundError("Can't find '{}', use arborize.add_directory to add a morphology directory.".format(morphology))
[docs]def import3d(file, model):
"""
Perform NEURON's Import3D and import ``file`` 3D data into the model.
"""
loaded_morphology = NeuronModel._import3d_load(file)
loaded_morphology.instantiate(model)
[docs]def make_builder(blueprint):
"""
Turn a blueprint (morphology string, builder function or tuple of the former)
into a Builder.
"""
if type(blueprint) is str:
# Use Import3D as builder.
return _import3d_load(blueprint)
if callable(blueprint):
# If a function is given as morphology, treat it as a builder function
return Builder(blueprint)
elif isinstance(blueprint, staticmethod):
# If a static method is given as morphology, treat it as a builder function
return Builder(blueprint.__func__)
elif (
hasattr(type(blueprint), "__len__")
and hasattr(type(blueprint), "__getitem__")
):
# If it is a sequence, construct a ComboBuilder that sequentially applies the builders.
return ComboBuilder(*blueprint)
else:
raise MorphologyBuilderException("Invalid blueprint data: provide a builder function or a path string to a morphology file.")