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 =