From 7f11061762e1b82994c5a57a4289b95c31eda65c Mon Sep 17 00:00:00 2001
From: asanin-epfl <53935643+asanin-epfl@users.noreply.github.com>
Date: Fri, 16 Jul 2021 10:49:14 +0200
Subject: [PATCH] Draw morphology with synapses (#82)
Move all plotting functions under `plot` package
---
CHANGELOG.rst | 5 ++
README.rst | 46 ++++++-----
doc/source/api.rst | 16 ++++
doc/source/conf.py | 8 ++
doc/source/index.rst | 1 +
examples/dendrogram.py | 20 +++--
examples/dendrogram_plain_example.swc | 5 +-
examples/draw_morphology.py | 80 ++++++++++++++++++
morph_tool/morphdb.py | 8 +-
morph_tool/nrnhines.py | 4 +-
morph_tool/plot/__init__.py | 7 ++
morph_tool/plot/consts.py | 38 +++++++++
morph_tool/{ => plot}/dendrogram.py | 33 +++++---
morph_tool/plot/morphology.py | 114 ++++++++++++++++++++++++++
tests/test_dendrogram.py | 2 +-
tests/test_morphdb.py | 2 +-
tests/test_utils.py | 3 +-
tox.ini | 1 +
18 files changed, 341 insertions(+), 52 deletions(-)
create mode 100644 doc/source/api.rst
create mode 100644 examples/draw_morphology.py
create mode 100644 morph_tool/plot/__init__.py
create mode 100644 morph_tool/plot/consts.py
rename morph_tool/{ => plot}/dendrogram.py (86%)
create mode 100644 morph_tool/plot/morphology.py
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ccf099e..3ac0458 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,11 @@
Changelog
=========
+Version 2.8.0
+-------------
+- Add functionality to plot morphologies with synapses. Move all plot functionality under
+ ``plot`` package. (#82)
+
Version 2.7.0
-------------
- Fix compatibility with numpy>=1.21 (#80)
diff --git a/README.rst b/README.rst
index 97e18ef..1b91b2c 100644
--- a/README.rst
+++ b/README.rst
@@ -1,3 +1,5 @@
+|docs|
+
MorphTool
=========
@@ -13,6 +15,14 @@ MorphIO v2 and NeuroM v1
------------------------
If you want to work with old NeuroM v1 and MorphIO v2, use the version 2.4.7 of morph-tool.
+Documentation
+-------------
+
+MorphIO documentation is built and hosted on `readthedocs `__.
+
+* `latest snapshot `_
+* `latest release `_
+
Installation
------------
It is recommended to install in a fresh virtualenv.
@@ -259,35 +269,27 @@ Splitting this section into 3 compartments would results in the following paths:
[2. , 2. ]]
-Dendrogram with synapses
-------------------------
+Plot morphologies with synapses
+-------------------------------
-This functionality is available only when the package is installed with **dendrogram** extras:
+This functionality is available only when the package is installed with **plot** extras:
.. code:: bash
- pip install morph-tool
-
-Draw NeuroM dendrogram with synapses on it. Synapses must be represented as a DataFrame. Required
-columns in this dataframe are:
-
-.. code:: python
-
- from morph_tool import dendrogram
- required_columns = [dendrogram.SOURCE_NODE_ID, dendrogram.TARGET_NODE_ID,
- dendrogram.POST_SECTION_ID, dendrogram.POST_SECTION_POS,
- dendrogram.PRE_SECTION_ID, dendrogram.PRE_SECTION_POS]
+ pip install morph-tool[plot]
-or equivalently
+Dendrogram
+~~~~~~~~~~
-.. code:: python
+Draw NeuroM dendrogram with synapses on it. Synapses must be represented as a DataFrame. See
+`dendrogram `__.
- required_columns = ['@source_node', '@target_node',
- 'afferent_section_id', 'afferent_section_pos',
- 'efferent_section_id', 'efferent_section_pos']
+Morphology
+~~~~~~~~~~
+Draw NeuroM morphology with synapses on it. Synapses must be represented as a DataFrame. See
+`morphology `__.
-For usage examples look at ``examples/dendrogram.py``.
Contributing
------------
@@ -309,3 +311,7 @@ morph-tool is licensed under the terms of the GNU Lesser General Public License
Refer to COPYING.LESSER and COPYING for details.
Copyright (c) 2018-2021 Blue Brain Project/EPFL
+
+.. |docs| image:: https://readthedocs.org/projects/morph-tool/badge/?version=latest
+ :target: https://morph-tool.readthedocs.io/
+ :alt: documentation status
diff --git a/doc/source/api.rst b/doc/source/api.rst
new file mode 100644
index 0000000..c9bd370
--- /dev/null
+++ b/doc/source/api.rst
@@ -0,0 +1,16 @@
+API Documentation
+=================
+
+.. toctree::
+ :hidden:
+
+ Introduction
+
+.. autosummary::
+ :nosignatures:
+ :toctree: _morph_tool_build
+
+ morph_tool.plot
+ morph_tool.plot.consts
+ morph_tool.plot.dendrogram
+ morph_tool.plot.morphology
diff --git a/doc/source/conf.py b/doc/source/conf.py
index de65f09..fe5b7c7 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -24,6 +24,9 @@
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
+ 'sphinx.ext.napoleon',
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.autosummary',
]
# Add any paths that contain templates here, relative to this directory.
@@ -84,6 +87,11 @@
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
+autosummary_generate = True
+autosummary_imported_members = False
+autosummary_mock_imports = ['neuron', 'plotly']
+autodoc_default_options = {'members': True}
+
# -- Options for HTML output ---------------------------------------------------
diff --git a/doc/source/index.rst b/doc/source/index.rst
index a4a908d..590041a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -6,4 +6,5 @@
Home
morphdb
+ api
changelog
diff --git a/examples/dendrogram.py b/examples/dendrogram.py
index e678bd5..cfd5a2c 100644
--- a/examples/dendrogram.py
+++ b/examples/dendrogram.py
@@ -1,17 +1,21 @@
import numpy as np
import pandas as pd
import neurom as nm
-from morph_tool import dendrogram
+from morph_tool.plot import dendrogram, consts
def plain_example():
- """Example that shows how to draw a neuron dendrogram with a plain synapses dataframe."""
+ """Example that shows how to plot a neuron dendrogram with a plain synapses dataframe."""
# Those properties are required in synapses dataframe for positioning
required_synapse_properties = [
- dendrogram.SOURCE_NODE_ID, dendrogram.TARGET_NODE_ID,
- dendrogram.POST_SECTION_ID, dendrogram.POST_SECTION_POS,
- dendrogram.PRE_SECTION_ID, dendrogram.PRE_SECTION_POS,
+ consts.SOURCE_NODE_ID, consts.TARGET_NODE_ID,
+ consts.POST_SECTION_ID, consts.POST_SECTION_POS,
+ consts.PRE_SECTION_ID, consts.PRE_SECTION_POS,
]
+ # or use plain strings
+ # required_columns = ['@source_node', '@target_node',
+ # 'afferent_section_id', 'afferent_section_pos',
+ # 'efferent_section_id', 'efferent_section_pos']
data = np.array([
[0, 116, 4, 0.81408846, 3, 0.7344886],
[0, 116, 5, 0.145983203, 4, 0.24454929],
@@ -20,6 +24,8 @@ def plain_example():
[116, 0, 2, 0.5815143, 1, 0.68261607],
])
synapses = pd.DataFrame(columns=required_synapse_properties, data=data)
+ synapses = synapses.astype({'@target_node': int, '@source_node': int,
+ 'afferent_section_id': int, 'efferent_section_id': int})
neuron = nm.load_neuron('dendrogram_plain_example.swc')
fig = dendrogram.draw(neuron, synapses, 116)
fig.show()
@@ -43,7 +49,7 @@ def plain_example():
def circuit_example():
- """Example that shows how to draw a neuron dendrogram with synapses from a bluepysnap circuit.
+ """Example that shows how to plot a neuron dendrogram with synapses from a bluepysnap circuit.
To make this example work, you would need a proper SONATA circuit.
"""
@@ -51,7 +57,7 @@ def circuit_example():
from bluepysnap.sonata_constants import Edge
from bluepysnap.bbp import Synapse
circuit = Circuit('/path/to/sonata_circuit_config.json')
- # you can use `bluepysnap.sonata_constants.Edge` instead of `dendrogram` for position constants
+ # you can use `bluepysnap.sonata_constants.Edge` instead of `morph_tool.draw.consts`
edge_properties = [
Edge.SOURCE_NODE_ID, Edge.TARGET_NODE_ID,
Edge.POST_SECTION_ID, Edge.POST_SECTION_POS,
diff --git a/examples/dendrogram_plain_example.swc b/examples/dendrogram_plain_example.swc
index 90c5dc3..1663dc9 100644
--- a/examples/dendrogram_plain_example.swc
+++ b/examples/dendrogram_plain_example.swc
@@ -5,6 +5,7 @@
5 3 6 5 0 0. 3
6 2 0 0 0 1. 1
7 2 0 -4 0 1. 6
- 8 2 6 -4 0 0. 7
- 9 2 -5 -4 0 0. 7
+ 8 2 6 -4 0 1. 7
+ 9 2 -5 -4 0 .5 7
+ 10 2 -6 -5 0 .5 9
# index, type, x, y, z, radius, parent
diff --git a/examples/draw_morphology.py b/examples/draw_morphology.py
new file mode 100644
index 0000000..836ba1d
--- /dev/null
+++ b/examples/draw_morphology.py
@@ -0,0 +1,80 @@
+import numpy as np
+import pandas as pd
+import neurom as nm
+from morph_tool.plot import morphology
+from morph_tool.plot import consts
+
+
+def example_afferent():
+ neuron = nm.load_neuron('dendrogram_plain_example.swc')
+ data = np.array([
+ [4, 0, 0.81408846, 'additional value'],
+ [6, 2, 0.545983203, 'additional value'],
+ [1, 0, 0.4290702, 'additional value'],
+ ])
+ columns = [consts.POST_SECTION_ID, consts.POST_SEGMENT_ID, consts.POST_SEGMENT_OFFSET,
+ 'additional_data']
+ synapses = pd.DataFrame(data, columns=columns)
+
+ fig = morphology.draw(neuron, synapses)
+ fig.show()
+
+
+def example_efferent():
+ neuron = nm.load_neuron('dendrogram_plain_example.swc')
+ data = np.array([
+ [4, 0, 0.81408846, 'additional value'],
+ [6, 2, 0.645983203, 'additional value'],
+ [1, 0, 0.4290702, 'additional value'],
+ ])
+ columns = [consts.PRE_SECTION_ID, consts.PRE_SEGMENT_ID, consts.PRE_SEGMENT_OFFSET,
+ 'additional_data']
+ synapses = pd.DataFrame(data, columns=columns)
+
+ fig = morphology.draw(neuron, synapses)
+ fig.show()
+
+
+def example_afferent_efferent():
+ neuron = nm.load_neuron('dendrogram_plain_example.swc')
+ data = np.array([
+ [4, 0, 0.81408846, np.nan, np.nan, np.nan],
+ [6, 2, 0.145983203, np.nan, np.nan, np.nan],
+ [np.nan, np.nan, np.nan, 1, 0, 0.4290702],
+ [np.nan, np.nan, np.nan, 1, 0, 0.29180855],
+ [np.nan, np.nan, np.nan, 1, 0, 0.68261607],
+ ])
+ columns = [consts.POST_SECTION_ID, consts.POST_SEGMENT_ID, consts.POST_SEGMENT_OFFSET,
+ consts.PRE_SECTION_ID, consts.PRE_SEGMENT_ID, consts.PRE_SEGMENT_OFFSET]
+ synapses = pd.DataFrame(data, columns=columns)
+
+ fig = morphology.draw(neuron, synapses)
+ fig.show()
+
+
+def circuit_example():
+ """Example that shows how to plot a morphology with synapses from a Sonata circuit.
+
+ To make this example work, you would need a proper SONATA circuit.
+ """
+ from bluepysnap import Circuit
+ from bluepysnap.bbp import Synapse
+ circuit = Circuit('/path/to/sonata_circuit_config.json')
+ edge_properties = [
+ 'afferent_section_id', 'afferent_segment_id', 'afferent_segment_offset',
+ Synapse.U_SYN, Synapse.D_SYN, Synapse.F_SYN, Synapse.G_SYNX,
+ ]
+ morph_id = 110
+ synapses = circuit.edges['default'].afferent_edges(morph_id, edge_properties)
+ morph_filepath = circuit.nodes['All'].morph.get_filepath(morph_id)
+ neuron = nm.load_neuron(morph_filepath)
+
+ fig = morphology.draw(neuron, synapses)
+ fig.show()
+
+
+if __name__ == '__main__':
+ # circuit_example()
+ # example_afferent()
+ example_efferent()
+ # example_afferent_efferent()
diff --git a/morph_tool/morphdb.py b/morph_tool/morphdb.py
index 23568f9..ad7cf37 100644
--- a/morph_tool/morphdb.py
+++ b/morph_tool/morphdb.py
@@ -179,14 +179,14 @@ def _from_neurondb_dat(cls, neurondb, morph_paths, label):
obj.df['mtype_no_subtype'] = fulltypes[0]
obj.df['msubtype'] = fulltypes[1]
else:
- obj.df['mtype_no_subtype'] = obj.df.mtype
+ obj.df['mtype_no_subtype'] = obj.df['mtype']
obj.df['msubtype'] = ''
obj.df['label'] = label
for missing_col in set(COLUMNS) - set(obj.df.columns):
obj.df[missing_col] = None
obj.df.layer = obj.df.layer.astype('str')
- obj.df['path'] = obj.df.name.map(morph_paths)
+ obj.df['path'] = obj.df['name'].map(morph_paths)
obj.df = obj.df.reindex(columns=COLUMNS)
for key in BOOLEAN_REPAIR_ATTRS:
obj.df[key] = True
@@ -339,7 +339,7 @@ def features(self, config: Dict, n_workers=1):
df = self.df.copy().reset_index(drop=True)
df.columns = pd.MultiIndex.from_product((["properties"], df.columns.values))
- stats = extract_dataframe(df['properties', 'path'], config, n_workers)
+ stats = extract_dataframe(df.loc[:, ('properties', 'path')], config, n_workers)
return df.join(stats.drop(columns='name', level=1), how='inner')
def check_files_exist(self):
@@ -349,7 +349,7 @@ def check_files_exist(self):
raise ValueError(
f'DataFrame has morphologies with undefined filepaths: {missing_morphs}')
- for path in self.df.path:
+ for path in self.df['path']:
if not path.exists():
raise ValueError(f'Non existing path: {path}')
diff --git a/morph_tool/nrnhines.py b/morph_tool/nrnhines.py
index e7213c2..1f3080d 100644
--- a/morph_tool/nrnhines.py
+++ b/morph_tool/nrnhines.py
@@ -24,9 +24,9 @@ def get_NRN_cell(filename: Path):
try:
# pylint: disable=import-outside-toplevel
from bluepyopt import ephys
- except ImportError as e:
+ except ImportError as e_:
raise ImportError(
- 'bluepyopt not installed; please use `pip install morph-tool[nrn]`') from e
+ 'bluepyopt not installed; please use `pip install morph-tool[nrn]`') from e_
m = ephys.morphologies.NrnFileMorphology(str(filename))
sim = ephys.simulators.NrnSimulator()
cell = ephys.models.CellModel('test', morph=m, mechs=[])
diff --git a/morph_tool/plot/__init__.py b/morph_tool/plot/__init__.py
new file mode 100644
index 0000000..a81fd05
--- /dev/null
+++ b/morph_tool/plot/__init__.py
@@ -0,0 +1,7 @@
+"""Module for plotting functions. In order to use it, you must install morph-tool as:
+
+.. code:: bash
+
+ pip install morph-tool[plot]
+
+"""
diff --git a/morph_tool/plot/consts.py b/morph_tool/plot/consts.py
new file mode 100644
index 0000000..3470878
--- /dev/null
+++ b/morph_tool/plot/consts.py
@@ -0,0 +1,38 @@
+"""Constants for names that are required to be in columns of ``synapses`` arg of
+``morph_tool.plot`` package. These constants are the same as edge properties of `Sonata
+`__. So you can use these constants, their values as plain
+strings or constants from `bluepysnap `__ library:
+
+An example of using constants or plain strings:
+ .. literalinclude:: ../../../examples/dendrogram.py
+ :pyobject: plain_example
+
+An example of using with a circuit and bluepysnap constants:
+ .. literalinclude:: ../../../examples/dendrogram.py
+ :pyobject: circuit_example
+
+"""
+
+#: Contains string `@target_node` that must be used as column name in `synapses` arg.
+TARGET_NODE_ID = '@target_node'
+#: Contains string `@source_node` that must be used as column name in `synapses` arg.
+SOURCE_NODE_ID = '@source_node'
+
+#: Contains string `afferent_section_id` that must be used as column name in `synapses` arg.
+POST_SECTION_ID = 'afferent_section_id'
+#: Contains string `afferent_section_pos` that must be used as column name in `synapses` arg.
+POST_SECTION_POS = 'afferent_section_pos'
+#: Contains string `afferent_segment_id` that must be used as column name in `synapses` arg.
+POST_SEGMENT_ID = 'afferent_segment_id'
+#: Contains string `afferent_segment_offset` that must be used as column name in `synapses` arg.
+POST_SEGMENT_OFFSET = 'afferent_segment_offset'
+
+#: Contains string `efferent_section_id` that must be used as column name in `synapses` arg.
+PRE_SECTION_ID = 'efferent_section_id'
+#: Contains string `efferent_section_pos` that must be used as column name in `synapses` arg.
+PRE_SECTION_POS = 'efferent_section_pos'
+#: Contains string `efferent_segment_id` that must be used as column name in `synapses` arg.
+PRE_SEGMENT_ID = 'efferent_segment_id'
+#: Contains string `efferent_segment_offset` that must be used as column name in `synapses` arg.
+PRE_SEGMENT_OFFSET = 'efferent_segment_offset'
diff --git a/morph_tool/dendrogram.py b/morph_tool/plot/dendrogram.py
similarity index 86%
rename from morph_tool/dendrogram.py
rename to morph_tool/plot/dendrogram.py
index ab891d9..a431fbd 100644
--- a/morph_tool/dendrogram.py
+++ b/morph_tool/plot/dendrogram.py
@@ -1,9 +1,10 @@
-'''Dendrogram helper functions and class'''
+"""Module for drawing dendrograms with synapses."""
import numpy as np
from neurom import NeuriteType
from neurom.core import Neurite, Neuron
from neurom.view.dendrogram import Dendrogram, layout_dendrogram, move_positions, get_size
from neurom.view.view import TREE_COLOR
+from pandas.core.dtypes.common import is_integer_dtype
try:
import plotly.express as px
@@ -13,12 +14,9 @@
'morph-tool[plot] is not installed. Please install: pip install morph-tool[plot]'
) from e
-POST_SECTION_ID = 'afferent_section_id'
-POST_SECTION_POS = 'afferent_section_pos'
-TARGET_NODE_ID = '@target_node'
-PRE_SECTION_ID = 'efferent_section_id'
-PRE_SECTION_POS = 'efferent_section_pos'
-SOURCE_NODE_ID = '@source_node'
+from morph_tool.plot.consts import (SOURCE_NODE_ID, TARGET_NODE_ID,
+ POST_SECTION_ID, POST_SECTION_POS,
+ PRE_SECTION_ID, PRE_SECTION_POS)
class SynDendrogram(Dendrogram):
@@ -145,21 +143,30 @@ def _get_synapse_colormap(synapses):
return {id_: color_list[idx % len(color_list)] for idx, id_ in enumerate(node_id_set)}
-def draw(neuron, synapses=None, neuron_node_id=None):
+def draw(morphology, synapses=None, neuron_node_id=None):
"""Draw dendrogram with synapses.
Args:
- neuron (Neurite|Neuron): a Neurite or a Neuron instance of NeuroM package.
+ morphology (Neurite|Neuron): a Neuron instance of NeuroM package.
synapses (DataFrame): synapses dataframe.
- neuron_node_id (int|None): node id of ``neuron``. If None then it is taken from
+ neuron_node_id (int|None): node id of ``morphology``. If None then it is taken from
``synapses[TARGET_NODE_ID]``.
Returns:
plotly.graph_objects.Figure: plotly figure
+
+ Example
+ .. literalinclude:: ../../../examples/dendrogram.py
+ :pyobject: plain_example
+
"""
- synapses = synapses.astype({'@target_node': int, '@source_node': int,
- 'afferent_section_id': int, 'efferent_section_id': int})
- dendrogram = SynDendrogram(neuron)
+ assert (is_integer_dtype(synapses[TARGET_NODE_ID].dtype) and
+ is_integer_dtype(synapses[SOURCE_NODE_ID].dtype) and
+ is_integer_dtype(synapses[POST_SECTION_ID].dtype) and
+ is_integer_dtype(synapses[PRE_SECTION_ID].dtype)), \
+ 'Section ids and nodes columns of `synapses` arg must be integers'
+
+ dendrogram = SynDendrogram(morphology)
positions = layout_dendrogram(dendrogram, np.array([0, 0]))
w, h = get_size(positions)
positions = move_positions(positions, np.array([.5 * w, 0]))
diff --git a/morph_tool/plot/morphology.py b/morph_tool/plot/morphology.py
new file mode 100644
index 0000000..cf7242d
--- /dev/null
+++ b/morph_tool/plot/morphology.py
@@ -0,0 +1,114 @@
+"""Module for drawing morphologies with synapses."""
+
+from functools import partial
+import warnings
+
+import pandas as pd
+from pandas import Categorical
+from pandas.core.dtypes.common import is_integer_dtype
+from plotly import express
+
+from neurom import morphmath, COLS
+from neurom.view.plotly import get_figure
+from morph_tool.plot.consts import (PRE_SECTION_ID, PRE_SEGMENT_ID, PRE_SEGMENT_OFFSET,
+ POST_SECTION_ID, POST_SEGMENT_ID, POST_SEGMENT_OFFSET)
+
+REQUIRED_PRE_COLUMNS = {PRE_SECTION_ID, PRE_SEGMENT_ID, PRE_SEGMENT_OFFSET}
+REQUIRED_POST_COLUMNS = {POST_SECTION_ID, POST_SEGMENT_ID, POST_SEGMENT_OFFSET}
+
+
+def _validate_synapses(synapses):
+ def _is_int(*columns):
+ return all(is_integer_dtype(synapses[c].dtype) for c in columns)
+
+ is_pre = REQUIRED_PRE_COLUMNS.issubset(synapses.columns)
+ is_post = REQUIRED_POST_COLUMNS.issubset(synapses.columns)
+ assert is_pre or is_post, \
+ f'{REQUIRED_PRE_COLUMNS} or {REQUIRED_POST_COLUMNS} are required columns of `synapses`'
+ if is_pre and not _is_int(PRE_SECTION_ID, PRE_SEGMENT_ID) or \
+ is_post and not _is_int(POST_SECTION_ID, POST_SEGMENT_ID):
+ warnings.warn('Section ids and segment ids columns of `synapses` arg are not integers, and '
+ 'will be forced to integer type')
+
+
+def _add_coords(synapse, morphology):
+ """Adds coordinates and direction fields to ``synapses`` via ``apply`` function."""
+ is_pre = PRE_SECTION_ID in synapse.index and not pd.isnull(synapse[PRE_SECTION_ID])
+ is_post = POST_SECTION_ID in synapse.index and not pd.isnull(synapse[POST_SECTION_ID])
+ assert is_pre != is_post, 'Synapse must have either afferent or efferent section ids for the ' \
+ 'morphology. It cant have both at the same time.'
+
+ if is_post:
+ sec_id = int(synapse[POST_SECTION_ID])
+ seg_id = int(synapse[POST_SEGMENT_ID])
+ seg_ofst = float(synapse[POST_SEGMENT_OFFSET])
+ synapse['direction'] = 'afferent'
+ else:
+ sec_id = int(synapse[PRE_SECTION_ID])
+ seg_id = int(synapse[PRE_SEGMENT_ID])
+ seg_ofst = float(synapse[PRE_SEGMENT_OFFSET])
+ synapse['direction'] = 'efferent'
+
+ if sec_id == 0:
+ # synapse is on soma
+ p = morphology.soma.points[0]
+ synapse['x'], synapse['y'], synapse['z'] = p[COLS.XYZ]
+ # place synapse on surface of soma along Z axes so it won't be hidden by soma on the plot
+ synapse['z'] += p[COLS.R]
+ return synapse
+
+ # NeuroM morphology counts sections from 0. Synapses count sections from 1 because section
+ # id 0 is for soma.
+ sec_id -= 1
+ sec = morphology.sections[sec_id]
+ assert sec_id == sec.id, f'Error. Synapse with section id {sec_id} must map to the same ' \
+ f'section id in `morphology` arg but maps to {sec.id}.'
+ assert 0 <= seg_id <= len(sec.points), f'No such segment id {seg_id} for section id ' \
+ f'{sec_id} of `morphology` arg'
+
+ seg_p1, seg_p2 = sec.points[seg_id - 1], sec.points[seg_id]
+ seg_len = morphmath.point_dist(seg_p1, seg_p2)
+ coords = morphmath.linear_interpolate(seg_p1, seg_p2, seg_ofst / seg_len)
+ synapse['x'], synapse['y'], synapse['z'] = coords
+ return synapse
+
+
+def draw(morphology, synapses):
+ """Draw morphology with synapses.
+
+ Args:
+ morphology (Neuron): a Neuron instance of NeuroM package.
+ synapses (DataFrame): synapses dataframe. It is required to have columns from
+ :py:mod:`morph_tool.plot.consts`. See an example for details.
+
+ Returns:
+ plotly.graph_objects.Figure: plotly figure
+
+ Afferent only synapses example
+ .. literalinclude:: ../../../examples/draw_morphology.py
+ :pyobject: example_afferent
+
+ Afferent and efferent synapses example
+ .. literalinclude:: ../../../examples/draw_morphology.py
+ :pyobject: example_afferent_efferent
+
+ Circuit synapses example
+ .. literalinclude:: ../../../examples/draw_morphology.py
+ :pyobject: circuit_example
+
+ """
+ _validate_synapses(synapses)
+ synapses['direction'] = Categorical(['None'] * len(synapses),
+ categories=['afferent', 'efferent', 'None'])
+ synapses = synapses.apply(partial(_add_coords, morphology=morphology), axis=1)
+
+ fig = express.scatter_3d(synapses, x='x', y='y', z='z',
+ color='direction',
+ color_discrete_map={'afferent': 'orange', 'efferent': 'green',
+ 'None': 'black'},
+ hover_data=synapses.columns)
+
+ figure_dict = get_figure(morphology, plane='3d', title=morphology.name)
+ fig.add_traces(figure_dict['data'])
+ fig.update_layout(figure_dict['layout'])
+ return fig
diff --git a/tests/test_dendrogram.py b/tests/test_dendrogram.py
index 32e4038..e6ebf1a 100644
--- a/tests/test_dendrogram.py
+++ b/tests/test_dendrogram.py
@@ -7,7 +7,7 @@
from nose.tools import assert_raises
-from morph_tool import dendrogram
+from morph_tool.plot import dendrogram
DATA = Path(__file__).resolve().parent / 'data'
diff --git a/tests/test_morphdb.py b/tests/test_morphdb.py
index 9371250..2e04fb8 100644
--- a/tests/test_morphdb.py
+++ b/tests/test_morphdb.py
@@ -160,7 +160,7 @@ def test_features():
for key in tested.BOOLEAN_REPAIR_ATTRS:
expected['properties', key] = expected['properties', key].astype(bool)
assert_frame_equal(features.drop(columns=('properties', 'axon_inputs')),
- expected.drop(columns=('properties', 'axon_inputs')))
+ expected.drop(columns=('properties', 'axon_inputs')), check_dtype=False)
def test_check_file_exists():
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 16dce02..e7ca43c 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -47,8 +47,7 @@ def test_find_morph():
folder = DATA / 'test-neurondb-with-path'
assert_equal(tested.find_morph(folder, 'not-here.h5'),
None)
- assert_equal(tested.find_morph(folder, 'C270106A'),
- folder / 'C270106A.h5')
+ assert tested.find_morph(folder, 'C270106A').samefile(folder / 'C270106A.h5')
assert_equal(tested.find_morph(folder, 'C270106C.wrongext'),
None)
diff --git a/tox.ini b/tox.ini
index 9a08f34..3f91c41 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,6 +4,7 @@ testdeps =
mock
nose
bluepysnap>=0.5
+ pandas<=1.2.5 # 1.3.0 contains an error that breaks tests, waiting for 1.3.1
[tox]
envlist =