From a2aa4b424c742334aab94ff6e48a4fe967fc59f8 Mon Sep 17 00:00:00 2001 From: asanin-epfl <53935643+asanin-epfl@users.noreply.github.com> Date: Mon, 30 Aug 2021 10:40:20 +0200 Subject: [PATCH] 3.0.0 (#950) New major release. See CHANGELOG.rst for more details on changes. - rename neuron to morphology - refactor features - refactor viewer - allow pass arguments to neurom stats --- CHANGELOG.rst | 45 + MANIFEST.in | 2 + doc/source/api.rst | 20 +- doc/source/examples.rst | 18 +- doc/source/features.rst | 123 ++ doc/source/index.rst | 5 +- .../{migration_v2.rst => migration.rst} | 66 +- doc/source/morph_check.rst | 8 +- doc/source/morph_stats.rst | 134 +- doc/source/quickstart.rst | 50 +- doc/source/tutorial.rst | 6 +- examples/boxplot.py | 10 +- examples/density_plot.py | 10 +- examples/end_to_end_distance.py | 10 +- examples/extract_distribution.py | 6 +- examples/features_graph_table.py | 10 +- examples/get_features.py | 42 +- examples/histogram.py | 20 +- ...tion_analysis.py => iteration_analysis.py} | 32 +- examples/nl_fst_compat.py | 26 +- examples/plot_features.py | 10 +- examples/plot_somas.py | 14 +- examples/radius_of_gyration.py | 10 +- examples/section_ids.py | 4 +- examples/soma_radius_fit.py | 8 +- examples/synthesis_json.py | 8 +- neurom/__init__.py | 29 +- neurom/apps/cli.py | 45 +- neurom/apps/config/morph_check.yaml | 2 +- neurom/apps/config/morph_stats.yaml | 8 +- neurom/apps/morph_stats.py | 195 ++- neurom/check/__init__.py | 2 +- neurom/check/morphology_checks.py | 346 +++++ neurom/check/morphtree.py | 34 +- neurom/check/neuron_checks.py | 355 +---- neurom/check/runner.py | 17 +- neurom/check/structural_checks.py | 46 - neurom/core/__init__.py | 2 +- neurom/core/morphology.py | 489 +++++++ neurom/core/neuron.py | 469 +----- neurom/core/population.py | 46 +- neurom/core/tree.py | 32 - neurom/core/types.py | 8 +- neurom/features/__init__.py | 151 +- neurom/features/bifurcation.py | 186 +++ neurom/features/bifurcationfunc.py | 190 +-- neurom/features/morphology.py | 296 ++++ neurom/features/neurite.py | 504 +++++++ neurom/features/neuritefunc.py | 613 -------- neurom/features/neuronfunc.py | 300 ---- neurom/features/population.py | 82 ++ neurom/features/section.py | 165 +++ neurom/features/sectionfunc.py | 169 +-- neurom/geom/transform.py | 2 +- neurom/io/utils.py | 77 +- neurom/utils.py | 54 +- neurom/view/__init__.py | 8 +- neurom/view/dendrogram.py | 7 +- neurom/view/{view.py => matplotlib_impl.py} | 123 +- .../view/{common.py => matplotlib_utils.py} | 1 + neurom/view/plotly.py | 145 -- neurom/view/plotly_impl.py | 173 +++ neurom/viewer.py | 52 +- pylintrc | 2 +- setup.py | 2 +- tests/__init__.py | 0 tests/apps/test_annotate.py | 8 +- tests/apps/test_cli.py | 38 +- tests/apps/test_config.py | 2 +- tests/apps/test_morph_stats.py | 285 ++-- ...on_checks.py => test_morphology_checks.py} | 224 ++- tests/check/test_morphtree.py | 60 +- tests/check/test_runner.py | 16 +- tests/check/test_structural_checks.py | 34 - tests/core/test_iter.py | 32 +- tests/core/test_neurite.py | 6 +- tests/core/test_neuron.py | 81 +- tests/core/test_population.py | 10 +- tests/core/test_section.py | 38 +- tests/core/test_soma.py | 28 +- tests/core/test_tree.py | 34 - tests/data/extracted-stats.csv | 4 +- tests/features/__init__.py | 0 ...bifurcationfunc.py => test_bifurcation.py} | 12 +- tests/features/test_feature_compat.py | 220 --- tests/features/test_get_features.py | 1284 +++++++---------- tests/features/test_morphology.py | 280 ++++ tests/features/test_neurite.py | 244 ++++ tests/features/test_neuritefunc.py | 397 ----- tests/features/test_neuronfunc.py | 241 ---- .../{test_sectionfunc.py => test_section.py} | 111 +- tests/features/utils.py | 19 - tests/geom/test_geom.py | 6 +- tests/geom/test_transform.py | 92 +- tests/io/test_io_utils.py | 203 ++- tests/io/test_neurolucida.py | 30 +- tests/io/test_swc_reader.py | 24 +- tests/test_utils.py | 23 +- tests/test_viewer.py | 107 +- tests/view/conftest.py | 6 +- tests/view/test_dendrogram.py | 32 +- .../{test_view.py => test_matplotlib_impl.py} | 117 +- ...est_common.py => test_matplotlib_utils.py} | 6 +- tests/view/test_plotly_impl.py | 50 + tutorial/getting_started.ipynb | 29 +- tutorial/plotly.ipynb | 80 - 106 files changed, 5124 insertions(+), 5513 deletions(-) create mode 100644 doc/source/features.rst rename doc/source/{migration_v2.rst => migration.rst} (57%) rename examples/{neuron_iteration_analysis.py => iteration_analysis.py} (86%) create mode 100644 neurom/check/morphology_checks.py delete mode 100644 neurom/check/structural_checks.py create mode 100644 neurom/core/morphology.py delete mode 100644 neurom/core/tree.py create mode 100644 neurom/features/bifurcation.py create mode 100644 neurom/features/morphology.py create mode 100644 neurom/features/neurite.py delete mode 100644 neurom/features/neuritefunc.py delete mode 100644 neurom/features/neuronfunc.py create mode 100644 neurom/features/population.py create mode 100644 neurom/features/section.py rename neurom/view/{view.py => matplotlib_impl.py} (82%) rename neurom/view/{common.py => matplotlib_utils.py} (99%) delete mode 100644 neurom/view/plotly.py create mode 100644 neurom/view/plotly_impl.py create mode 100644 tests/__init__.py rename tests/check/{test_neuron_checks.py => test_morphology_checks.py} (62%) delete mode 100644 tests/check/test_structural_checks.py delete mode 100644 tests/core/test_tree.py create mode 100644 tests/features/__init__.py rename tests/features/{test_bifurcationfunc.py => test_bifurcation.py} (93%) delete mode 100644 tests/features/test_feature_compat.py create mode 100644 tests/features/test_morphology.py create mode 100644 tests/features/test_neurite.py delete mode 100644 tests/features/test_neuritefunc.py delete mode 100644 tests/features/test_neuronfunc.py rename tests/features/{test_sectionfunc.py => test_section.py} (59%) delete mode 100644 tests/features/utils.py rename tests/view/{test_view.py => test_matplotlib_impl.py} (60%) rename tests/view/{test_common.py => test_matplotlib_utils.py} (96%) create mode 100644 tests/view/test_plotly_impl.py delete mode 100644 tutorial/plotly.ipynb diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d771e4a7c..e3ad2dc3b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,51 @@ Changelog ========= +Version 3.0.0 +------------- +- Rename all 'neuron' names to 'morphology' names, including module and package names. Previous + 'neuron' names still exist but deprecated. It is recommended to use new names: + + - ``neurom.check.neuron_checks`` => ``neurom.check.morphology_checks``, replace `neuron_checks` + with `morphology_checks` in configs for ``neurom check``. + - ``neurom.core.neuron`` => ``neurom.core.morphology`` + - ``neurom.core.neuron.Neuron`` => ``neurom.core.morphology.Morphology`` + - ``neurom.core.neuron.graft_neuron`` => ``neurom.core.morphology.graft_morphology`` + - ``neurom.io.utils.load_neuron`` => ``neurom.io.utils.load_morphology`` + - ``neurom.io.utils.load_neurons`` => ``neurom.io.utils.load_morphologies`` + - ``neurom.core.Population.neurons`` => ``neurom.core.Population.morphologies`` + +- Refactor plotting functionality. :ref:`migration-v3.0.0`. + - deprecate ``neurom.view.viewer`` + - rename ``neurom.view.view`` to ``neurom.view.matplotlib_impl`` + - rename ``neurom.view.plotly`` to ``neurom.view.plotly_impl`` + - rename ``neurom.view.common`` to ``neurom.view.matplotlib_utils`` + - swap arguments ``ax`` and ``nrn`` of all plot functions in ``neurom.view.matplotlib_impl``, + also ``nrn`` arg is renamed to ``morph``. + - delete ``neurom.view.plotly.draw``. Use instead ``neurom.view.plotly_impl.plot_morph`` and + ``neurom.view.plotly_impl.plot_morph3d``. + +- Refactor features. + - Drop 'func' suffix of all module names within `features` package: + - ``neurom.features.bifurcationfunc`` => ``neurom.features.bifurcation`` + - ``neurom.features.sectionfunc`` => ``neurom.features.section`` + - ``neurom.features.neuritefunc`` => ``neurom.features.neurite`` + - ``neurom.features.neuronfunc`` => ``neurom.features.morphology`` + - Rigid classification of features. ``neurite`` features must accept only a single neurite. + ``morphology`` features must accept only a single morphology. ``population`` features must + accept only a collection of neurons or a neuron population. + - Some features were deleted, renamed, added. See :ref:`migration-v3.0.0`. + - Name consistency among private variables. + - Delete deprecated `neurom.features.register_neurite_feature`. + +- Refactor morphology statistics, e.g. ``neurom stats`` command. + - New config format. See :ref:`morph-stats-new-config`. The old format is still supported. + The only necessary change is replace 'total' with 'sum', 'neuron' with 'morphology'. + - Keep feature names as is. Don't trim 's' at the end of plurals. + +- Delete ``neurom.check.structural_checks``, ``neurom.core.tree`` that were deprecated in v2. +- Delete unused ``neurom.utils.memoize`` + Version 2.3.1 ------------- - fix ``features.neuronfunc._neuron_population`` for 'sholl_frequency' feature over a neuron diff --git a/MANIFEST.in b/MANIFEST.in index 0abbc7005..c160149af 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ include requirements.txt include requirements_dev.txt include README.rst +recursive-exclude tests * +recursive-exclude tutorial * graft neurom/config diff --git a/doc/source/api.rst b/doc/source/api.rst index 62aa9313c..48c256e62 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -40,19 +40,21 @@ API Documentation neurom.morphmath neurom.features - neurom.features.neuronfunc - neurom.features.neuritefunc - neurom.features.sectionfunc - neurom.features.bifurcationfunc + neurom.features.population + neurom.features.morphology + neurom.features.neurite + neurom.features.section + neurom.features.bifurcation neurom.check.morphtree - neurom.check.neuron_checks + neurom.check.morphology_checks neurom.core.types - neurom.core.neuron + neurom.core.morphology neurom.core.population neurom.core.soma neurom.core.dataformat neurom.io.utils neurom.view - neurom.view.common - neurom.view.view - neurom.viewer + neurom.view.dendrogram + neurom.view.matplotlib_utils + neurom.view.matplotlib_impl + neurom.view.plotly_impl diff --git a/doc/source/examples.rst b/doc/source/examples.rst index 4cb0b8cb8..979a8bcfc 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -38,30 +38,30 @@ Examples Fast analysis with :py:mod:`neurom` *********************************** -Here we load a neuron and obtain some information from it: +Here we load a morphology and obtain some information from it: .. code-block:: python >>> import neurom as nm - >>> nrn = nm.load_neuron('some/data/path/morph_file.swc') - >>> ap_seg_len = nm.get('segment_lengths', nrn, neurite_type=nm.APICAL_DENDRITE) - >>> ax_sec_len = nm.get('section_lengths', nrn, neurite_type=nm.AXON) + >>> m = nm.load_morphology('some/data/path/morph_file.swc') + >>> ap_seg_len = nm.get('segment_lengths', m, neurite_type=nm.APICAL_DENDRITE) + >>> ax_sec_len = nm.get('section_lengths', m, neurite_type=nm.AXON) Morphology visualization with the :py:mod:`neurom.viewer` module **************************************************************** -Here we visualize a neuronal morphology: +Here we visualize a morphology: .. code-block:: python - >>> # Initialize nrn as above + >>> # Initialize m as above >>> from neurom import viewer - >>> fig, ax = viewer.draw(nrn) + >>> fig, ax = viewer.draw(m) >>> fig.show() >>> - >>> fig, ax = viewer.draw(nrn, mode='3d') # valid modes '2d', '3d', 'dendrogram' + >>> fig, ax = viewer.draw(m, mode='3d') # valid modes '2d', '3d', 'dendrogram' >>> fig.show() Advanced iterator-based feature extraction example @@ -79,7 +79,7 @@ All of the examples in the previous sections can be implemented in a similar way to those presented here. -.. literalinclude:: ../../examples/neuron_iteration_analysis.py +.. literalinclude:: ../../examples/iteration_analysis.py :lines: 30- Getting Log Information diff --git a/doc/source/features.rst b/doc/source/features.rst new file mode 100644 index 000000000..afc49ed45 --- /dev/null +++ b/doc/source/features.rst @@ -0,0 +1,123 @@ +.. Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project + All rights reserved. + + This file is part of NeuroM + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +.. _features: + +Features +******** +A tool for analysing of morphologies. It allows to extract various information about morphologies. +For example if you need to know the segment lengths of a morphology then you need to call +``segment_lengths`` feature. The complete list of available features is spread among pages +:mod:`neurom.features.neurite`, :mod:`neurom.features.morphology`, +:mod:`neurom.features.population`. + +Features are spread among ``neurite``, ``morphology``, ``population`` modules to emphasize their +expected input. Features from ``neurite`` expect a neurite as their input. So calling it with +a morphology input will fail. ``morphology`` expects a morphology only. ``population`` expects a +population only. + +This restriction can be bypassed if you call a feature from ``neurite`` via the features +mechanism ``features.get``. However the mechanism does not allow to appply ``population`` +features to anything other than a morphology population, and ``morphology`` features can be applied +only to a morphology or a morphology population. + +An example for ``neurite``: + +.. code-block:: python + + from neurom import load_morphology, features + from neurom.features.neurite import max_radial_distance + + m = load_morphology('path/to/morphology') + # valid input + max_radial_distance(m.neurites[0]) + # invalid input + max_radial_distance(m) + # valid input + features.get('max_radial_distance', m) + +The features mechanism assumes that a neurite feature must be summed if it returns a number, and +concatenated if it returns a list. Other types of returns are invalid. For example lets take +a feature ``number_of_segments`` of ``neurite``. It returns a number of segments in a neurite. +Calling it on a morphology will return a sum of ``number_of_segments`` of all the morphology's neurites. +Calling it on a morphology population will return a list of ``number_of_segments`` of each morphology +within the population. + + +.. code-block:: python + + from neurom import load_morphology, features + + m = load_morphology('path/to/morphology') + # a single number + features.get('number_of_segments', m.neurites[0]) + # a single number that is a sum for all `m.neurites`. + features.get('number_of_segments', m) + + pop = load_morphology('path/to/morphology population') + # a list of numbers + features.get('number_of_segments', pop) + +if a list is returned then the feature results are concatenated. + +.. code-block:: python + + from neurom import load_morphology, features + + m = load_morphology('path/to/morphology') + # a list of lengths in a neurite + features.get('section_lengths', m.neurites[0]) + # a flat list of lengths in a morphology, no separation among neurites + features.get('section_lengths', m) + + pop = load_morphology('path/to/morphology population') + # a flat list of lengths in a population, no separation among morphologies + features.get('section_lengths', pop) + +In case such implicit behaviour does not work a feature can be rewritten for each input separately. +For example, a feature ``max_radial_distance`` that requires a `max` operation instead of implicit +`sum`. Its definition in ``neurite``: + +.. literalinclude:: ../../neurom/features/neurite.py + :pyobject: max_radial_distance + +In order to make it work for a morphology, it is redefined in ``morphology``: + +.. literalinclude:: ../../neurom/features/morphology.py + :pyobject: max_radial_distance + +Another feature that requires redefining is ``sholl_frequency``. This feature applies different +logic for a morphology and a morphology population. That is why it is defined in ``morphology``: + +.. literalinclude:: ../../neurom/features/morphology.py + :pyobject: sholl_frequency + +and redefined in ``population`` + +.. literalinclude:: ../../neurom/features/population.py + :pyobject: sholl_frequency diff --git a/doc/source/index.rst b/doc/source/index.rst index 4c560d1fd..9df30f1e5 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -33,7 +33,7 @@ NeuroM ====== -NeuroM is a Python-based toolkit for the analysis and processing of neuron morphologies. +NeuroM is a Python-based toolkit for the analysis and processing of morphologies. .. toctree:: :hidden: @@ -43,12 +43,13 @@ NeuroM is a Python-based toolkit for the analysis and processing of neuron morph install validation tutorial + features examples cli definitions api developer documentation - migration_v2 + migration changelog license diff --git a/doc/source/migration_v2.rst b/doc/source/migration.rst similarity index 57% rename from doc/source/migration_v2.rst rename to doc/source/migration.rst index a17c14e15..b1661241e 100644 --- a/doc/source/migration_v2.rst +++ b/doc/source/migration.rst @@ -26,11 +26,67 @@ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -.. _migration-v2: - +Migration guides ======================= + +.. _migration-v3.0.0: + +Migration to v3 version +----------------------- + +- ``neurom.view.viewer`` is deprecated. To get the same results as before, use the replacement: + + .. code-block:: python + + import neurom as nm + # instead of: from neurom import viewer + from neurom.view import matplotlib_impl, matplotlib_utils + m = nm.load_morphology('some/data/path/morph_file.asc') + + # instead of: viewer.draw(m) + matplotlib_impl.plot_morph(m) + + # instead of: viewer.draw(m, mode='3d') + matplotlib_impl.plot_morph3d(m) + + # instead of: viewer.draw(m, mode='dendrogram') + matplotlib_impl.plot_dendrogram(m) + + # If you used ``output_path`` with any of functions above then the solution is: + fig, ax = matplotlib_utils.get_figure() + matplotlib_impl.plot_dendrogram(m, ax) + matplotlib_utils.plot_style(fig=fig, ax=ax) + matplotlib_utils.save_plot(fig=fig, output_path=output_path) + # for other plots like `plot_morph` it is the same, you just need to call `plot_morph` instead + # of `plot_dendrogram`. + + # instead of `plotly.draw` + from neurom import plotly_impl + plotly_impl.plot_morph(m) # for 2d + plotly_impl.plot_morph3d(m) # for 3d + +- breaking features changes: + - use `max_radial_distance` instead of `max_radial_distances` + - use `number_of_segments` instead of `n_segments` + - use `number_of_neurites` instead of `n_neurites` + - use `number_of_sections` instead of `n_sections` + - use `number_of_bifurcations` instead of `n_bifurcation_points` + - use `number_of_forking_points` instead of `n_forking_points` + - use `number_of_leaves` instead of `number_of_terminations`, `n_leaves` + - use `soma_radius` instead of `soma_radii` + - use `soma_surface_area` instead of `soma_surface_areas` + - use `soma_volume` instead of `soma_volumes` + - use `total_length_per_neurite` instead of `neurite_lengths` + - use `total_volume_per_neurite` instead of `neurite_volumes` + - use `terminal_path_lengths` instead of `terminal_path_lengths_per_neurite` + - use `bifurcation_partitions` instead of `partition` + - new neurite feature `total_area` that complements `total_area_per_neurite` + - new neurite feature `volume_density` that complements `neurite_volume_density` + + Migration to v2 version -======================= +----------------------- +.. _migration-v2: - ``Neuron`` object now extends ``morphio.Morphology``. - NeuroM does not remove unifurcations on load. Unifurcation is a section with a single child. Such @@ -40,10 +96,10 @@ Migration to v2 version .. code-block:: python import neurom as nm - nrn = nm.load_neuron('some/data/path/morph_file.asc') + nrn = nm.load_morphology('some/data/path/morph_file.asc') nrn.remove_unifurcations() -- Soma is not considered as a section anymore. Soma is skipped when iterating over neuron's +- Soma is not considered as a section anymore. Soma is skipped when iterating over morphology's sections. It means that section indexing offset needs to be adjusted by ``-(number of soma sections)`` which is usually ``-1``. - drop ``benchmarks`` diff --git a/doc/source/morph_check.rst b/doc/source/morph_check.rst index 8de3a7cc7..74d61dacb 100644 --- a/doc/source/morph_check.rst +++ b/doc/source/morph_check.rst @@ -43,7 +43,7 @@ An example usage The tests are grouped in two categories: 1. Structural tests. **Dropped in v2 version**. -2. Neuron tests. These are applied to properties of reconstructed neurons and their +2. Morphology tests. These are applied to properties of reconstructed morphologies and their constituent soma and neurites, and can be thought of as "quality" checks. @@ -58,7 +58,7 @@ mentioned above. Here is an example configuration: .. code-block:: yaml checks: - neuron_checks: + morphology_checks: - has_basal_dendrite - has_axon - has_all_nonzero_segment_lengths @@ -74,8 +74,8 @@ mentioned above. Here is an example configuration: As can be seen, the configuration file is split into two sections ``checks``, and ``options``. -``checks`` can only have `neuron_checks` sub-item that corresponds to a sub-module -:py:mod:`neuron_checks`. Each of its sub-items corresponds to a function +``checks`` can only have `morphology_checks` sub-item that corresponds to a sub-module +:py:mod:`morphology_checks`. Each of its sub-items corresponds to a function in that sub-module. diff --git a/doc/source/morph_stats.rst b/doc/source/morph_stats.rst index b8aab1f42..21201a3d0 100644 --- a/doc/source/morph_stats.rst +++ b/doc/source/morph_stats.rst @@ -29,10 +29,10 @@ neurom stats: morphometric statistics extraction ************************************************ -The ``neurom stats`` application extracts morphometrics from a set of neuron morphology +The ``neurom stats`` application extracts morphometrics from a set of morphology files and produces a summary in JSON or CSV format. It may obtain any of the morphometrics available in the :py:func:`neurom.get` function, and is highly configurable, allowing the user to get -raw or summary statistics from a large set of neurite and neuron features. +raw or summary statistics from a large set of neurite and morphology features. An example usage: @@ -40,9 +40,23 @@ An example usage: neurom stats path/to/morph/file_or_dir --config path/to/config --output path/to/output/file +For more information on the application and available options, invoke it with the ``--help`` +or ``-h`` option. + +.. code-block:: bash + + neurom stats --help + The functionality can be best explained by looking at a sample configuration file that is supposed to go under ``--config`` option: +Config +------ + +Short format (prior version 3.0.0) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +An example config: + .. code-block:: yaml neurite: @@ -61,7 +75,7 @@ to go under ``--config`` option: - ALL neuron: - soma_radii: + soma_radius: - mean @@ -69,18 +83,20 @@ Here, there are two feature categories, 1. ``neurite``: these are morphometrics obtained from neurites, e.g. branch orders, section lengths, bifurcation angles, path lengths. -2. ``neuron``: these are morphometrics that can be applied to a whole neuron, e.g. the soma radius, +2. ``neuron``: these are morphometrics that can be applied to a whole morphology, e.g. the soma radius, the trunk radii, etc. -Each category sub-item (section_lengths, soma_radii, etc) corresponds to a -:py:func:`neurom.get` feature, and each one of its sub-items corresponds to a statistic, e.g. +Each category sub-item (section_lengths, soma_radius, etc) corresponds to a +:py:func:`neurom.get` feature, and each one of its sub-items corresponds to a statistic aggregating +function, e.g. * ``raw``: array of raw values * ``max``, ``min``, ``mean``, ``median``, ``std``: self-explanatory. * ``total``: sum of the raw values An additional field ``neurite_type`` specifies the neurite types into which the morphometrics -are to be split. This is a sample output using the above configuration: +are to be split. It applies only to ``neurite`` features. A sample output using the above +configuration: .. code-block:: json @@ -88,42 +104,102 @@ are to be split. This is a sample output using the above configuration: "some/path/morph.swc":{ "mean_soma_radius":0.17071067811865476, "axon":{ - "total_section_length":207.87975220908129, - "max_section_length":11.018460736176685, - "max_section_branch_order":10, - "total_section_volume":276.73857657289523 + "sum_section_lengths":207.87975220908129, + "max_section_lengths":11.018460736176685, + "max_section_branch_orders":10, + "sum_section_volumes":276.73857657289523 }, "all":{ - "total_section_length":840.68521442251949, - "max_section_length":11.758281556059444, - "max_section_branch_order":10, - "total_section_volume":1104.9077419665782 + "sum_section_lengths":840.68521442251949, + "max_section_lengths":11.758281556059444, + "max_section_branch_orders":10, + "sum_section_volumes":1104.9077419665782 }, "apical_dendrite":{ - "total_section_length":214.37304577550353, - "max_section_length":11.758281556059444, - "max_section_branch_order":10, - "total_section_volume":271.9412385728449 + "sum_section_lengths":214.37304577550353, + "max_section_lengths":11.758281556059444, + "max_section_branch_orders":10, + "sum_section_volumes":271.9412385728449 }, "basal_dendrite":{ - "total_section_length":418.43241643793476, - "max_section_length":11.652508126101711, - "max_section_branch_order":10, - "total_section_volume":556.22792682083821 + "sum_section_lengths":418.43241643793476, + "max_section_lengths":11.652508126101711, + "max_section_branch_orders":10, + "sum_section_volumes":556.22792682083821 } } } +.. _morph-stats-new-config: -For more information on the application and available options, invoke it with the ``--help`` -or ``-h`` option. +Kwargs format (starting version 3.0.0) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The new format: -.. code-block:: bash +- requires to use ``morphology`` instead of ``neuron`` key in the config. +- requires to use ``sum`` instead of ``total`` statistic aggregating function. +- allows to specify features arguments. + +For example, ``partition_asymmetry`` feature has additional arguments like ``method`` and +``variant`` (see :py:func:`neurom.features.neurite.partition_asymmetries`). Before it wasn't +possible to set them. Here is how you can set them now: + +.. code-block:: yaml + + neurite: + partition_asymmetry: + kwargs: + variant: 'length' + method: 'petilla' + modes: + - max + - sum + +Instead of statistic aggregating functions right after a feature name, config expects ``kwargs`` +and ``modes`` properties. The former sets the feature arguments. The latter sets the statistic +aggregating function. This allows to set ``neurite_type`` directly on the feature, and overwrites +global setting of neurite types via ``neurite_type`` global config field. For example: + +.. code-block:: yaml + + neurite: + section_lengths: + kwargs: + neurite_type: APICAL_DENDRITE + modes: + - max + - sum + +So the example config from `Short format (prior version 3.0.0)`_ looks: + +.. code-block:: yaml + + neurite: + section_lengths: + modes: + - max + - sum + section_volumes: + modes: + - sum + section_branch_orders: + modes: + - max + + neurite_type: + - AXON + - APICAL_DENDRITE + - BASAL_DENDRITE + - ALL + + morphology: + soma_radius: + modes: + - mean - neurom stats --help Features -------- -All available features for ``--config`` are documented in :mod:`neurom.features.neuronfunc` and -:mod:`neurom.features.neuritefunc`. +All available features for ``--config`` are documented in :mod:`neurom.features.morphology`, +:mod:`neurom.features.neurite`, :mod:`neurom.features.population`. diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst index 41754b1ce..d3d72488a 100644 --- a/doc/source/quickstart.rst +++ b/doc/source/quickstart.rst @@ -46,48 +46,32 @@ Analyze, visualize, and check ============================= The :mod:`neurom` module has various helper functions and command line applications -to simplify loading neuron morphologies from files into ``neurom`` data structures and -obtaining morphometrics, either from single or multiple neurons. +to simplify loading morphologies from files into ``neurom`` data structures and +obtaining morphometrics, either from single or multiple morphologies. The functionality described here is limited, but it is hoped that it will suffice for most analyses. Extract morphometrics with :func:`neurom.features.get` ------------------------------------------------------ -These are some of the properties can be obtained for a single neurite type or for all -neurites regardless of type via :func:`neurom.features.get`: - -* Segment lengths -* Section lengths -* Segment radii -* Number of sections -* Number of sections per neurite -* Number of neurites -* Number of segments -* Local and remote bifurcation angles -* Section path distances -* Section radial distances -* Section branch orders -* Total neurite length - -The usage is simple: +Analyze morphologies via :func:`neurom.features.get`. This way you can get things like segment +lengths, section lengths, etc. .. code:: import neurom as nm - nrn = nm.load_neuron('some/data/path/morph_file0.swc') - nrn_ap_seg_len = nm.features.get('segment_lengths', nrn, neurite_type=nm.APICAL_DENDRITE) - pop = nm.load_neurons('some/data/path') + m = nm.load_morphology('some/data/path/morph_file0.swc') + m_ap_seg_len = nm.features.get('segment_lengths', m, neurite_type=nm.APICAL_DENDRITE) + pop = nm.load_morphologies('some/data/path') pop_ap_seg_len = nm.features.get('segment_lengths', pop, neurite_type=nm.APICAL_DENDRITE) -This function also allows obtaining the soma radius and surface area. - +For more details see :ref:`features`. -Iterate over neurites with :func:`neurom.core.neuron.iter_neurites` -------------------------------------------------------------------- +Iterate over neurites with :func:`neurom.core.morphology.iter_neurites` +----------------------------------------------------------------------- -:func:`neurom.core.neuron.iter_neurites` function allows to iterate over the neurites -of a single neuron or a neuron population. It can also be applied to a single +:func:`neurom.core.morphology.iter_neurites` function allows to iterate over the neurites +of a single morphology or a morphology population. It can also be applied to a single neurite or a list of neurites. It allows to optionally pass a function to be mapped onto each neurite, as well as a neurite filter function. In this example, we apply a simple user defined function to the apical dendrites in a population: @@ -102,13 +86,13 @@ we apply a simple user defined function to the apical dendrites in a population: stuff = [x for x in nm.iter_neurites(pop, user_func, lambda n : n.type == nm.APICAL_DENDRITE)] -View neurons with :mod:`neurom.viewer` --------------------------------------- +View morphologies with :mod:`neurom.viewer` +------------------------------------------- -There are also helper functions to plot a neuron in 2 and 3 dimensions. +There are also helper functions to plot a morphology in 2 and 3 dimensions. :func:`neurom.viewer.draw` function allows the user to make two and three-dimensional -plots of neurites, somata and neurons. It also has a dendrogram neuron plotting mode. +plots of neurites, somata and morphologies. It also has a dendrogram morphology plotting mode. Extract morphometrics into JSON files @@ -164,7 +148,7 @@ Check data validity The :doc:`neurom check` application applies some semantic checks to morphology data files in order to -determine whether it is suitable to construct a neuron structure and whether certain +determine whether it is suitable to construct a morphology structure and whether certain defects within the structure are detected. It can be invoked from the command line, and takes as main argument the path to either a single file or a directory of morphology files. diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 784417508..6016d8d71 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -34,8 +34,7 @@ a folder that contains the NeuroM Tutorial notebook. (nrm)$ jupyter notebook # launch the Jupyter Notebook App Next, you can select the notebook that you want to open. Now, you can go -through the tutorial and learn about loading, viewing, and analyzing -neuronal morphologies! +through the tutorial and learn about loading, viewing, and analyzing morphologies! Applications using NeuroM ========================= @@ -55,8 +54,7 @@ it into NeuroM: (nrm)$ neurom check --help # shows help for morphology checking script -Try it yourself! You can go to -`NeuroMorpho.Org `__ to download a neuronal +Try it yourself! You can go to `NeuroMorpho.Org `__ to download a morphology and perform the semantic checks: .. code-block:: bash diff --git a/examples/boxplot.py b/examples/boxplot.py index 2bc111deb..a78f71b7a 100644 --- a/examples/boxplot.py +++ b/examples/boxplot.py @@ -27,13 +27,13 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Box Plot function for multiple neurons.""" +"""Box Plot function for multiple morphs.""" -from neurom.view import common +from neurom.view import matplotlib_utils def boxplot(neurons, feature, new_fig=True, subplot=False): - """Plot a histogram of the selected feature for the population of neurons. + """Plot a histogram of the selected feature for the population of morphologies. Plots x-axis versus y-axis on a scatter|histogram|binned values plot. More information about the plot and how it works. @@ -41,7 +41,7 @@ def boxplot(neurons, feature, new_fig=True, subplot=False): Parameters ---------- neurons : list - List of Neurons. Single neurons must be encapsulated in a list. + List of Neurons. Single morphologies must be encapsulated in a list. feature : str The feature of interest. @@ -55,7 +55,7 @@ def boxplot(neurons, feature, new_fig=True, subplot=False): """ feature_values = [getattr(neu, 'get_' + feature)() for neu in neurons] - _, ax = common.get_figure(new_fig=new_fig, subplot=subplot) + _, ax = matplotlib_utils.get_figure(new_fig=new_fig, subplot=subplot) ax.boxplot(feature_values) diff --git a/examples/density_plot.py b/examples/density_plot.py index 50c6bdf84..6e008eaa7 100644 --- a/examples/density_plot.py +++ b/examples/density_plot.py @@ -32,7 +32,7 @@ import numpy as np from neurom import get as get_feat -from neurom.view import (common, view) +from neurom.view import matplotlib_utils, matplotlib_impl from neurom.core.types import NeuriteType @@ -54,7 +54,7 @@ def plot_density(population, # pylint: disable=too-many-arguments, too-many-loc """Plots the 2d histogram of the center coordinates of segments in the selected plane. """ - fig, ax = common.get_figure(new_fig=new_fig, subplot=subplot) + fig, ax = matplotlib_utils.get_figure(new_fig=new_fig, subplot=subplot) H1, xedges1, yedges1 = extract_density(population, plane=plane, bins=bins, neurite_type=neurite_type) @@ -77,7 +77,7 @@ def plot_density(population, # pylint: disable=too-many-arguments, too-many-loc kwargs['xlabel'] = kwargs.get('xlabel', plane[0]) kwargs['ylabel'] = kwargs.get('ylabel', plane[1]) - return common.plot_style(fig=fig, ax=ax, **kwargs) + return matplotlib_utils.plot_style(fig=fig, ax=ax, **kwargs) def plot_neuron_on_density(population, # pylint: disable=too-many-arguments @@ -89,9 +89,9 @@ def plot_neuron_on_density(population, # pylint: disable=too-many-arguments coordinates of segments in the selected plane and superimposes the view of the first neurite of the collection. """ - _, ax = common.get_figure(new_fig=new_fig) + _, ax = matplotlib_utils.get_figure(new_fig=new_fig) - view.plot_tree(ax, population.neurites[0]) + matplotlib_impl.plot_tree(population.neurites[0], ax) return plot_density(population, plane=plane, bins=bins, new_fig=False, subplot=subplot, colorlabel=colorlabel, labelfontsize=labelfontsize, levels=levels, diff --git a/examples/end_to_end_distance.py b/examples/end_to_end_distance.py index 38c9353c9..070a02e0a 100755 --- a/examples/end_to_end_distance.py +++ b/examples/end_to_end_distance.py @@ -74,19 +74,19 @@ def _dist(seg): if __name__ == '__main__': # load a neuron from an SWC file filename = 'tests/data/swc/Neuron_3_random_walker_branches.swc' - nrn = nm.load_neuron(filename) + m = nm.load_morphology(filename) # print mean end-to-end distance per neurite type print('Mean end-to-end distance for axons: ', - mean_end_to_end_dist(n for n in nrn.neurites if n.type == nm.AXON)) + mean_end_to_end_dist(n for n in m.neurites if n.type == nm.AXON)) print('Mean end-to-end distance for basal dendrites: ', - mean_end_to_end_dist(n for n in nrn.neurites if n.type == nm.BASAL_DENDRITE)) + mean_end_to_end_dist(n for n in m.neurites if n.type == nm.BASAL_DENDRITE)) print('Mean end-to-end distance for apical dendrites: ', - mean_end_to_end_dist(n for n in nrn.neurites + mean_end_to_end_dist(n for n in m.neurites if n.type == nm.APICAL_DENDRITE)) print('End-to-end distance per neurite (nb segments, end-to-end distance, neurite type):') - for nrte in nrn.neurites: + for nrte in m.neurites: # plot end-to-end distance for increasingly larger parts of neurite calculate_and_plot_end_to_end_distance(nrte) # print (number of segments, end-to-end distance, neurite type) diff --git a/examples/extract_distribution.py b/examples/extract_distribution.py index 5204b6f37..128957bce 100755 --- a/examples/extract_distribution.py +++ b/examples/extract_distribution.py @@ -28,7 +28,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Extract a distribution for the selected feature of the population of neurons among +"""Extract a distribution for the selected feature of the population of morphologies among the exponential, normal and uniform distribution, according to the minimum ks distance. """ @@ -57,12 +57,12 @@ def parse_args(): def extract_data(data_path, feature): - """Loads a list of neurons, extracts feature + """Loads a list of morphologies, extracts feature and transforms the fitted distribution in the correct format. Returns the optimal distribution, corresponding parameters, minimun and maximum values. """ - population = nm.load_neurons(data_path) + population = nm.load_morphologies(data_path) feature_data = [nm.get(feature, n) for n in population] feature_data = list(chain(*feature_data)) diff --git a/examples/features_graph_table.py b/examples/features_graph_table.py index b781997d7..95a98ea4a 100755 --- a/examples/features_graph_table.py +++ b/examples/features_graph_table.py @@ -66,12 +66,12 @@ def stylize(ax, name, feature): def histogram(neuron, feature, ax, bins=15, normed=True, cumulative=False): """ - Plot a histogram of the selected feature for the population of neurons. + Plot a histogram of the selected feature for the population of morphologies. Plots x-axis versus y-axis on a scatter|histogram|binned values plot. Parameters : - neurons : neuron list + morphologies : neuron list feature : str The feature of interest. @@ -110,10 +110,10 @@ def plot_feature(feature, cell): args = parse_args() for morph_file in get_morph_files(args.datapath): - nrn = nm.load_neuron(morph_file) + m = nm.load_morphology(morph_file) for _feature in args.features: - f = plot_feature(_feature, nrn) - figname = "{0}_{1}.eps".format(_feature, nrn.name) + f = plot_feature(_feature, m) + figname = "{0}_{1}.eps".format(_feature, m.name) f.savefig(Path(args.odir, figname)) pl.close(f) diff --git a/examples/get_features.py b/examples/get_features.py index 94fec9eef..1b784b842 100755 --- a/examples/get_features.py +++ b/examples/get_features.py @@ -65,12 +65,12 @@ def pprint_stats(data): filename = 'tests/data/swc/Neuron.swc' # load a neuron from an SWC file - nrn = nm.load_neuron(filename) + m = nm.load_morphology(filename) # Get some soma information # Soma radius and surface area - print("Soma radius", nm.get('soma_radii', nrn)[0]) - print("Soma surface area", nm.get('soma_surface_areas', nrn)[0]) + print("Soma radius", nm.get('soma_radii', m)[0]) + print("Soma surface area", nm.get('soma_surface_areas', m)[0]) # Get information about neurites # Most neurite data can be queried for a particular type of neurite. @@ -81,35 +81,35 @@ def pprint_stats(data): # to warm up... # number of neurites - print('Number of neurites (all):', nm.get('number_of_neurites', nrn)[0]) + print('Number of neurites (all):', nm.get('number_of_neurites', m)[0]) print('Number of neurites (axons):', - nm.get('number_of_neurites', nrn, neurite_type=nm.NeuriteType.axon)[0]) + nm.get('number_of_neurites', m, neurite_type=nm.NeuriteType.axon)[0]) print('Number of neurites (apical dendrites):', - nm.get('number_of_neurites', nrn, neurite_type=nm.NeuriteType.apical_dendrite)[0]) + nm.get('number_of_neurites', m, neurite_type=nm.NeuriteType.apical_dendrite)[0]) print('Number of neurites (basal dendrites):', - nm.get('number_of_neurites', nrn, neurite_type=nm.NeuriteType.basal_dendrite)[0]) + nm.get('number_of_neurites', m, neurite_type=nm.NeuriteType.basal_dendrite)[0]) # number of sections print('Number of sections:', - nm.get('number_of_sections', nrn)[0]) + nm.get('number_of_sections', m)[0]) print('Number of sections (axons):', - nm.get('number_of_sections', nrn, neurite_type=nm.NeuriteType.axon)[0]) + nm.get('number_of_sections', m, neurite_type=nm.NeuriteType.axon)[0]) print('Number of sections (apical dendrites):', - nm.get('number_of_sections', nrn, neurite_type=nm.NeuriteType.apical_dendrite)[0]) + nm.get('number_of_sections', m, neurite_type=nm.NeuriteType.apical_dendrite)[0]) print('Number of sections (basal dendrites):', - nm.get('number_of_sections', nrn, neurite_type=nm.NeuriteType.basal_dendrite)[0]) + nm.get('number_of_sections', m, neurite_type=nm.NeuriteType.basal_dendrite)[0]) # number of sections per neurite print('Number of sections per neurite:', - nm.get('number_of_sections_per_neurite', nrn)) + nm.get('number_of_sections_per_neurite', m)) print('Number of sections per neurite (axons):', - nm.get('number_of_sections_per_neurite', nrn, neurite_type=nm.NeuriteType.axon)) + nm.get('number_of_sections_per_neurite', m, neurite_type=nm.NeuriteType.axon)) print('Number of sections per neurite (apical dendrites):', nm.get('number_of_sections_per_neurite', - nrn, neurite_type=nm.NeuriteType.apical_dendrite)) + m, neurite_type=nm.NeuriteType.apical_dendrite)) print('Number of sections per neurite (basal dendrites):', nm.get('number_of_sections_per_neurite', - nrn, neurite_type=nm.NeuriteType.apical_dendrite)) + m, neurite_type=nm.NeuriteType.apical_dendrite)) # OK, this is getting repetitive, so lets loop over valid neurite types. # The following methods return arrays of measurements. We will gather some @@ -117,38 +117,38 @@ def pprint_stats(data): # Section lengths for all and different types of neurite for ttype in nm.NEURITE_TYPES: - sec_len = nm.get('section_lengths', nrn, neurite_type=ttype) + sec_len = nm.get('section_lengths', m, neurite_type=ttype) print('Section lengths (', ttype, '):', sep='') pprint_stats(sec_len) # Segment lengths for all and different types of neurite for ttype in nm.NEURITE_TYPES: - seg_len = nm.get('segment_lengths', nrn, neurite_type=ttype) + seg_len = nm.get('segment_lengths', m, neurite_type=ttype) print('Segment lengths (', ttype, '):', sep='') pprint_stats(seg_len) # Section radial distances for all and different types of neurite # Careful! Here we need to pass tree type as a named argument for ttype in nm.NEURITE_TYPES: - sec_rad_dist = nm.get('section_radial_distances', nrn, neurite_type=ttype) + sec_rad_dist = nm.get('section_radial_distances', m, neurite_type=ttype) print('Section radial distance (', ttype, '):', sep='') pprint_stats(sec_rad_dist) # Section path distances for all and different types of neurite # Careful! Here we need to pass tree type as a named argument for ttype in nm.NEURITE_TYPES: - sec_path_dist = nm.get('section_path_distances', nrn, neurite_type=ttype) + sec_path_dist = nm.get('section_path_distances', m, neurite_type=ttype) print('Section path distance (', ttype, '):', sep='') pprint_stats(sec_path_dist) # Local bifurcation angles for all and different types of neurite for ttype in nm.NEURITE_TYPES: - local_bifangles = nm.get('local_bifurcation_angles', nrn, neurite_type=ttype) + local_bifangles = nm.get('local_bifurcation_angles', m, neurite_type=ttype) print('Local bifurcation angles (', ttype, '):', sep='') pprint_stats(local_bifangles) # Remote bifurcation angles for all and different types of neurite for ttype in nm.NEURITE_TYPES: - rem_bifangles = nm.get('remote_bifurcation_angles', nrn, neurite_type=ttype) + rem_bifangles = nm.get('remote_bifurcation_angles', m, neurite_type=ttype) print('Local bifurcation angles (', ttype, '):', sep='') pprint_stats(rem_bifangles) diff --git a/examples/histogram.py b/examples/histogram.py index ce7497143..b45ba40d0 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -27,16 +27,16 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Simple Histogram function for multiple neurons.""" +"""Simple Histogram function for multiple morphologies.""" from itertools import chain import numpy as np -from neurom.view import common +from neurom.view import matplotlib_utils def histogram(neurons, feature, new_fig=True, subplot=False, normed=False, **kwargs): """ - Plot a histogram of the selected feature for the population of neurons. + Plot a histogram of the selected feature for the population of morphologies. Plots x-axis versus y-axis on a scatter|histogram|binned values plot. More information about the plot and how it works. @@ -44,7 +44,7 @@ def histogram(neurons, feature, new_fig=True, subplot=False, normed=False, **kwa Parameters : neurons : list - List of Neurons. Single neurons must be encapsulated in a list. + List of Neurons. Single morphologies must be encapsulated in a list. feature : str The feature of interest. @@ -73,7 +73,7 @@ def histogram(neurons, feature, new_fig=True, subplot=False, normed=False, **kwa bins = kwargs.get('bins', 25) cumulative = kwargs.get('cumulative', False) - fig, ax = common.get_figure(new_fig=new_fig, subplot=subplot) + fig, ax = matplotlib_utils.get_figure(new_fig=new_fig, subplot=subplot) kwargs['xlabel'] = kwargs.get('xlabel', feature) @@ -89,7 +89,7 @@ def histogram(neurons, feature, new_fig=True, subplot=False, normed=False, **kwa kwargs['no_legend'] = len(neu_labels) == 1 - return common.plot_style(fig=fig, ax=ax, **kwargs) + return matplotlib_utils.plot_style(fig=fig, ax=ax, **kwargs) def population_feature_values(pops, feature): @@ -99,7 +99,7 @@ def population_feature_values(pops, feature): for pop in pops: - feature_values = [getattr(neu, 'get_' + feature)() for neu in pop.neurons] + feature_values = [getattr(neu, 'get_' + feature)() for neu in pop.morphologies] # ugly hack to chain in case of list of lists if any([isinstance(p, (list, np.ndarray)) for p in feature_values]): @@ -113,7 +113,7 @@ def population_feature_values(pops, feature): def population_histogram(pops, feature, new_fig=True, normed=False, subplot=False, **kwargs): """ - Plot a histogram of the selected feature for the population of neurons. + Plot a histogram of the selected feature for the population of morphologies. Plots x-axis versus y-axis on a scatter|histogram|binned values plot. More information about the plot and how it works. @@ -149,7 +149,7 @@ def population_histogram(pops, feature, new_fig=True, normed=False, subplot=Fals bins = kwargs.get('bins', 25) cumulative = kwargs.get('cumulative', False) - fig, ax = common.get_figure(new_fig=new_fig, subplot=subplot) + fig, ax = matplotlib_utils.get_figure(new_fig=new_fig, subplot=subplot) kwargs['xlabel'] = kwargs.get('xlabel', feature) @@ -165,4 +165,4 @@ def population_histogram(pops, feature, new_fig=True, normed=False, subplot=Fals kwargs['no_legend'] = len(pops_labels) == 1 - return common.plot_style(fig=fig, ax=ax, **kwargs) + return matplotlib_utils.plot_style(fig=fig, ax=ax, **kwargs) diff --git a/examples/neuron_iteration_analysis.py b/examples/iteration_analysis.py similarity index 86% rename from examples/neuron_iteration_analysis.py rename to examples/iteration_analysis.py index 569a0fef8..a3a712e9b 100755 --- a/examples/neuron_iteration_analysis.py +++ b/examples/iteration_analysis.py @@ -38,7 +38,7 @@ from neurom.core.dataformat import COLS import neurom as nm from neurom import geom -from neurom.features import sectionfunc +from neurom.features import section from neurom.core import Section from neurom.core.types import tree_type_checker, NEURITES from neurom import morphmath as mm @@ -50,7 +50,7 @@ filename = 'tests/data/swc/Neuron.swc' # load a neuron from an SWC file - nrn = nm.load_neuron(filename) + m = nm.load_morphology(filename) # Some examples of what can be done using iteration # instead of pre-packaged functions that return lists. @@ -64,23 +64,23 @@ def sec_len(sec): return mm.section_length(sec.points) print('Total neurite length (sections):', - sum(sec_len(s) for s in nm.iter_sections(nrn))) + sum(sec_len(s) for s in nm.iter_sections(m))) # Get length of all neurites in cell by iterating over segments, # and summing the segment lengths. # This should yield the same result as iterating over sections. print('Total neurite length (segments):', - sum(mm.segment_length(s) for s in nm.iter_segments(nrn))) + sum(mm.segment_length(s) for s in nm.iter_segments(m))) # get volume of all neurites in cell by summing over segment # volumes print('Total neurite volume:', - sum(mm.segment_volume(s) for s in nm.iter_segments(nrn))) + sum(mm.segment_volume(s) for s in nm.iter_segments(m))) # get area of all neurites in cell by summing over segment # areas print('Total neurite surface area:', - sum(mm.segment_area(s) for s in nm.iter_segments(nrn))) + sum(mm.segment_area(s) for s in nm.iter_segments(m))) # get total number of neurite points in cell. def n_points(sec): @@ -90,32 +90,32 @@ def n_points(sec): return n if sec.parent is None else n - 1 print('Total number of points:', - sum(n_points(s) for s in nm.iter_sections(nrn))) + sum(n_points(s) for s in nm.iter_sections(m))) # get mean radius of neurite points in cell. # p[COLS.R] yields the radius for point p. # Note: this includes duplicated points at beginning of # non-trunk sections print('Mean radius of points:', - np.mean([s.points[:, COLS.R] for s in nm.iter_sections(nrn)])) + np.mean([s.points[:, COLS.R] for s in nm.iter_sections(m)])) # get mean radius of neurite points in cell. # p[COLS.R] yields the radius for point p. # Note: this includes duplicated points at beginning of # non-trunk sections - pts = [p[COLS.R] for s in nrn.sections[1:] for p in s.points] + pts = [p[COLS.R] for s in m.sections[1:] for p in s.points] print('Mean radius of points:', np.mean(pts)) # get mean radius of segments print('Mean radius of segments:', - np.mean(list(mm.segment_radius(s) for s in nm.iter_segments(nrn)))) + np.mean(list(mm.segment_radius(s) for s in nm.iter_segments(m)))) # get stats for the segment taper rate, for different types of neurite for ttype in NEURITES: ttt = ttype seg_taper_rate = [mm.segment_taper_rate(s) - for s in nm.iter_segments(nrn, neurite_filter=tree_type_checker(ttt))] + for s in nm.iter_segments(m, neurite_filter=tree_type_checker(ttt))] print('Segment taper rate (', ttype, '):\n mean=', np.mean(seg_taper_rate), @@ -126,19 +126,19 @@ def n_points(sec): # Number of bifurcation points. print('Number of bifurcation points:', - sum(1 for _ in nm.iter_sections(nrn, + sum(1 for _ in nm.iter_sections(m, iterator_type=Section.ibifurcation_point))) # Number of bifurcation points for apical dendrites print('Number of bifurcation points (apical dendrites):', - sum(1 for _ in nm.iter_sections(nrn, + sum(1 for _ in nm.iter_sections(m, iterator_type=Section.ibifurcation_point, neurite_filter=tree_type_checker(nm.APICAL_DENDRITE)))) # Maximum branch order print('Maximum branch order:', - max(sectionfunc.branch_order(s) for s in nm.iter_sections(nrn))) + max(section.branch_order(s) for s in nm.iter_sections(m))) - # Neuron's bounding box + # Morphology's bounding box # Note: does not account for soma radius - print('Bounding box ((min x, y, z), (max x, y, z))', geom.bounding_box(nrn)) + print('Bounding box ((min x, y, z), (max x, y, z))', geom.bounding_box(m)) diff --git a/examples/nl_fst_compat.py b/examples/nl_fst_compat.py index 1878e1d1f..b1607714d 100755 --- a/examples/nl_fst_compat.py +++ b/examples/nl_fst_compat.py @@ -32,28 +32,28 @@ import numpy as np import neurom as nm -from neurom.features import neuritefunc as _nf +from neurom.features import neurite as _nf -nrn_h5 = nm.load_neuron('tests/data/h5/v1/bio_neuron-001.h5') -nrn_asc = nm.load_neuron('tests/data/neurolucida/bio_neuron-001.asc') +m_h5 = nm.load_morphology('tests/data/h5/v1/bio_neuron-001.h5') +m_asc = nm.load_morphology('tests/data/neurolucida/bio_neuron-001.asc') -print('h5 number of sections: %s' % nm.get('number_of_sections', nrn_h5)[0]) -print('nl number of sections: %s\n' % nm.get('number_of_sections', nrn_asc)[0]) -print('h5 number of segments: %s' % nm.get('number_of_segments', nrn_h5)[0]) -print('nl number of segments: %s\n' % nm.get('number_of_segments', nrn_asc)[0]) +print('h5 number of sections: %s' % nm.get('number_of_sections', m_h5)[0]) +print('nl number of sections: %s\n' % nm.get('number_of_sections', m_asc)[0]) +print('h5 number of segments: %s' % nm.get('number_of_segments', m_h5)[0]) +print('nl number of segments: %s\n' % nm.get('number_of_segments', m_asc)[0]) print('h5 total neurite length: %s' % - np.sum(nm.get('section_lengths', nrn_h5))) + np.sum(nm.get('section_lengths', m_h5))) print('nl total neurite length: %s\n' % - np.sum(nm.get('section_lengths', nrn_asc))) + np.sum(nm.get('section_lengths', m_asc))) print('h5 principal direction extents: %s' % - nm.get('principal_direction_extents', nrn_h5)) + nm.get('principal_direction_extents', m_h5)) print('nl principal direction extents: %s' % - nm.get('principal_direction_extents', nrn_asc)) + nm.get('principal_direction_extents', m_asc)) print('\nNumber of neurites:') for nt in iter(nm.NeuriteType): - print(nt, _nf.n_neurites(nrn_h5, neurite_type=nt), _nf.n_neurites(nrn_asc, neurite_type=nt)) + print(nt, _nf.n_neurites(m_h5, neurite_type=nt), _nf.n_neurites(m_asc, neurite_type=nt)) print('\nNumber of segments:') for nt in iter(nm.NeuriteType): - print(nt, _nf.n_segments(nrn_h5, neurite_type=nt), _nf.n_segments(nrn_asc, neurite_type=nt)) + print(nt, _nf.n_segments(m_h5, neurite_type=nt), _nf.n_segments(m_asc, neurite_type=nt)) diff --git a/examples/plot_features.py b/examples/plot_features.py index a89c4f5a7..b5bb0f2c0 100755 --- a/examples/plot_features.py +++ b/examples/plot_features.py @@ -36,7 +36,7 @@ import numpy as np import neurom as nm -from neurom.view import common as view_utils +from neurom.view import matplotlib_utils import scipy.stats as _st from matplotlib.backends.backend_pdf import PdfPages @@ -112,13 +112,13 @@ def calc_limits(data, dist=None, padding=0.25): def load_neurite_features(filepath): """Unpack relevant data into megadict.""" stuff = defaultdict(lambda: defaultdict(list)) - nrns = nm.load_neurons(filepath) + morphs = nm.load_morphologies(filepath) # unpack data into arrays - for nrn in nrns: + for m in morphs: for t in NEURITES_: for feat in FEATURES: stuff[feat][str(t).split('.')[1]].extend( - nm.get(feat, nrn, neurite_type=t) + nm.get(feat, m, neurite_type=t) ) return stuff @@ -177,7 +177,7 @@ def main(data_dir, mtype_file): # pylint: disable=too-many-locals print('PLOT LIMITS:', limits) # print 'DATA:', data # print 'BIN HEIGHT', histo[0] - plot = Plot(*view_utils.get_figure(new_fig=True, subplot=111)) + plot = Plot(*matplotlib_utils.get_figure(new_fig=True, subplot=111)) plot.ax.set_xlim(*limits) plot.ax.bar(histo[1][:-1], histo[0], width=bin_widths(histo[1])) dp, bc = dist_points(histo[1], dist) diff --git a/examples/plot_somas.py b/examples/plot_somas.py index 60deb8090..0c7838ffb 100755 --- a/examples/plot_somas.py +++ b/examples/plot_somas.py @@ -31,8 +31,8 @@ from pathlib import Path -from neurom import load_neuron -import neurom.view.common as common +from neurom import load_morphology +from neurom.view import matplotlib_utils import matplotlib.pyplot as plt import numpy as np @@ -47,19 +47,19 @@ def random_color(): def plot_somas(somas): """Plot set of somas on same figure as spheres, each with different color.""" - _, ax = common.get_figure(new_fig=True, subplot=111, - params={'projection': '3d', 'aspect': 'equal'}) + _, ax = matplotlib_utils.get_figure(new_fig=True, subplot=111, + params={'projection': '3d', 'aspect': 'equal'}) for s in somas: - common.plot_sphere(ax, s.center, s.radius, color=random_color(), alpha=1) + matplotlib_utils.plot_sphere(ax, s.center, s.radius, color=random_color(), alpha=1) plt.show() if __name__ == '__main__': - # define set of files containing relevant neurons + # define set of files containing relevant morphs file_nms = [Path(SWC_PATH, file_nm) for file_nm in ['Soma_origin.swc', 'Soma_translated_1.swc', 'Soma_translated_2.swc']] # load from file and plot - sms = [load_neuron(file_nm).soma for file_nm in file_nms] + sms = [load_morphology(file_nm).soma for file_nm in file_nms] plot_somas(sms) diff --git a/examples/radius_of_gyration.py b/examples/radius_of_gyration.py index 063aed096..991dd9679 100755 --- a/examples/radius_of_gyration.py +++ b/examples/radius_of_gyration.py @@ -90,17 +90,17 @@ def mean_rad_of_gyration(neurites): if __name__ == '__main__': # load a neuron from an SWC file filename = 'tests/data/swc/Neuron.swc' - nrn = nm.load_neuron(filename) + m = nm.load_morphology(filename) # for every neurite, print (number of segments, radius of gyration, neurite type) print([(sum(len(s.points) - 1 for s in nrte.iter_sections()), - radius_of_gyration(nrte), nrte.type) for nrte in nrn.neurites]) + radius_of_gyration(nrte), nrte.type) for nrte in m.neurites]) # print mean radius of gyration per neurite type print('Mean radius of gyration for axons: ', - mean_rad_of_gyration(n for n in nrn.neurites if n.type == nm.AXON)) + mean_rad_of_gyration(n for n in m.neurites if n.type == nm.AXON)) print('Mean radius of gyration for basal dendrites: ', - mean_rad_of_gyration(n for n in nrn.neurites if n.type == nm.BASAL_DENDRITE)) + mean_rad_of_gyration(n for n in m.neurites if n.type == nm.BASAL_DENDRITE)) print('Mean radius of gyration for apical dendrites: ', - mean_rad_of_gyration(n for n in nrn.neurites + mean_rad_of_gyration(n for n in m.neurites if n.type == nm.APICAL_DENDRITE)) diff --git a/examples/section_ids.py b/examples/section_ids.py index 48966ccfa..754fac32f 100755 --- a/examples/section_ids.py +++ b/examples/section_ids.py @@ -46,9 +46,9 @@ def get_segment(neuron, section_id, segment_id): if __name__ == '__main__': - nrn = nm.load_neuron('tests/data/h5/v1/Neuron.h5') + m = nm.load_morphology('tests/data/h5/v1/Neuron.h5') - seg = get_segment(nrn, 3, 2) + seg = get_segment(m, 3, 2) print('Segment:\n', seg) print('Mid-point (x, y, z):\n', mm.linear_interpolate(seg[0], seg[1], 0.5)) print('Mid-point R:\n', mm.interpolate_radius(seg[0][COLS.R], seg[1][COLS.R], 0.5)) diff --git a/examples/soma_radius_fit.py b/examples/soma_radius_fit.py index b8a5a5118..3861cf081 100755 --- a/examples/soma_radius_fit.py +++ b/examples/soma_radius_fit.py @@ -28,8 +28,8 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Extract a distribution for the soma radii of the population (list) of neurons. - for the soma radii of the population (list) of neurons. +"""Extract a distribution for the soma radii of the population (list) of morphologies. + for the soma radii of the population (list) of morphologies. """ import argparse @@ -55,12 +55,12 @@ def test_multiple_distr(filepath): the optimal distribution along with the corresponding parameters. """ # load a neuron from an SWC file - population = nm.load_neurons(filepath) + population = nm.load_morphologies(filepath) # Create a list of basic distributions distr_to_check = ('norm', 'expon', 'uniform') - # Get the soma radii of a population of neurons + # Get the soma radii of a population of morphs soma_size = nm.get('soma_radii', population) # Find the best fit distribution diff --git a/examples/synthesis_json.py b/examples/synthesis_json.py index 32691ace7..4d67feb04 100755 --- a/examples/synthesis_json.py +++ b/examples/synthesis_json.py @@ -28,7 +28,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Extract the optimal distributions for the following features of the population of neurons: +"""Extract the optimal distributions for the following features of the population of morphologies: soma: radius basal dendrites: n_neurites apical dendrites: n_neurites @@ -44,7 +44,7 @@ import json from json import encoder -from neurom import get, load_neurons, NeuriteType, stats +from neurom import get, load_morphologies, NeuriteType, stats from neurom.io.utils import get_morph_files encoder.FLOAT_REPR = lambda o: format(o, '.2f') @@ -74,7 +74,7 @@ def extract_data(neurons, feature, params=None): - """Extracts feature from a list of neurons + """Extracts feature from a list of morphologies and transforms the fitted distribution in the correct format. Returns the optimal distribution and corresponding parameters. Normal distribution params (mean, std) @@ -106,7 +106,7 @@ def transform_package(mtype, files, components): features, feature_names, feature_components, feature_min, feature_max """ data_dict = transform_header(mtype) - neurons = load_neurons(files) + neurons = load_morphologies(files) for comp in components: params = PARAM_MAP[comp.name] diff --git a/neurom/__init__.py b/neurom/__init__.py index d538d8b3f..90290b560 100644 --- a/neurom/__init__.py +++ b/neurom/__init__.py @@ -29,39 +29,40 @@ """NeuroM neurom morphology analysis package. Examples: - Load a neuron + Load a morphology >>> import neurom as nm - >>> nrn = nm.load_neuron('some/data/path/morph_file.swc') + >>> m = nm.load_morphology('some/data/path/morph_file.swc') Obtain some morphometrics using the get function - >>> ap_seg_len = nm.get('segment_lengths', nrn, neurite_type=nm.APICAL_DENDRITE) - >>> ax_sec_len = nm.get('section_lengths', nrn, neurite_type=nm.AXON) + >>> ap_seg_len = nm.get('segment_lengths', m, neurite_type=nm.APICAL_DENDRITE) + >>> ax_sec_len = nm.get('section_lengths', m, neurite_type=nm.AXON) - Load neurons from a directory. This loads all SWC, HDF5 or NeuroLucida .asc\ - files it finds and returns a list of neurons + Load morphologies from a directory. This loads all SWC, HDF5 or NeuroLucida .asc + files it finds and returns a list of morphologies >>> import numpy as np # For mean value calculation - >>> nrns = nm.load_neurons('some/data/directory') - >>> for nrn in nrns: - ... print 'mean section length', np.mean(nm.get('section_lengths', nrn)) + >>> pop = nm.load_morphologies('some/data/directory') + >>> for m in pop: + ... print 'mean section length', np.mean(nm.get('section_lengths', m)) - Apply a function to a selection of neurites in a neuron or population. - This example gets the number of points in each axon in a neuron population + Apply a function to a selection of neurites in a morphology or morphology population. + This example gets the number of points in each axon in a morphology population >>> import neurom as nm >>> filter = lambda n : n.type == nm.AXON >>> mapping = lambda n : len(n.points) - >>> n_points = [n for n in nm.iter_neurites(nrns, mapping, filter)] + >>> n_points = [n for n in nm.iter_neurites(pop, mapping, filter)] """ from neurom.core.dataformat import COLS from neurom.core.types import NeuriteType, NeuriteIter, NEURITES as NEURITE_TYPES -from neurom.core.neuron import graft_neuron, iter_neurites, iter_sections, iter_segments +from neurom.core.morphology import graft_morphology, iter_neurites, iter_sections, iter_segments from neurom.features import get -from neurom.io.utils import NeuronLoader, load_neuron, load_neurons +from neurom.io.utils import MorphLoader, load_morphology, load_morphologies +from neurom.io.utils import load_neuron, load_neurons APICAL_DENDRITE = NeuriteType.apical_dendrite BASAL_DENDRITE = NeuriteType.basal_dendrite diff --git a/neurom/apps/cli.py b/neurom/apps/cli.py index 8d07b9185..8b2c8a786 100644 --- a/neurom/apps/cli.py +++ b/neurom/apps/cli.py @@ -28,13 +28,14 @@ """The morph-tool command line launcher.""" import logging +from functools import partial import click import matplotlib.pyplot as plt from neurom.apps import morph_stats, morph_check -from neurom import load_neuron -from neurom.viewer import draw as pyplot_draw +from neurom import load_morphology +from neurom.view import matplotlib_impl, matplotlib_utils @click.group() @@ -48,28 +49,34 @@ def cli(verbose): @cli.command() @click.argument('input_file') -@click.option('--plane', type=click.Choice(['3d', 'xy', 'yx', 'yz', 'zy', 'xz', 'zx']), - default='3d') -@click.option('--backend', type=click.Choice(['plotly', 'matplotlib']), - default='matplotlib') +@click.option('--3d', 'is_3d', is_flag=True) +@click.option('--plane', type=click.Choice(['xy', 'yx', 'yz', 'zy', 'xz', 'zx']), default='xy') +@click.option('--backend', type=click.Choice(['plotly', 'matplotlib']), default='matplotlib') @click.option('-r', '--realistic-diameters/--no-realistic-diameters', default=False, help='Scale diameters according to the plot axis\n' 'Warning: Only works with the matplotlib backend') -def view(input_file, plane, backend, realistic_diameters): - """A simple neuron viewer.""" +def view(input_file, is_3d, plane, backend, realistic_diameters): + """CLI interface to draw morphologies.""" # pylint: disable=import-outside-toplevel - if backend == 'matplotlib': - kwargs = { - 'mode': '3d' if plane == '3d' else '2d', - 'realistic_diameters': realistic_diameters, - } - if plane != '3d': - kwargs['plane'] = plane - pyplot_draw(load_neuron(input_file), **kwargs) - plt.show() + is_matplotlib = backend == 'matplotlib' + if is_matplotlib: + if is_3d: + _, ax = matplotlib_utils.get_figure(params={'projection': '3d'}) + plot = partial(matplotlib_impl.plot_morph3d, ax=ax) + else: + _, ax = matplotlib_utils.get_figure() + plot = partial(matplotlib_impl.plot_morph, ax=ax, + plane=plane, realistic_diameters=realistic_diameters) else: - from neurom.view.plotly import draw as plotly_draw - plotly_draw(load_neuron(input_file), plane=plane) + from neurom.view import plotly_impl + if is_3d: + plot = plotly_impl.plot_morph3d + else: + plot = partial(plotly_impl.plot_morph, plane=plane) + + plot(load_morphology(input_file)) + if is_matplotlib: + plt.show() @cli.command(short_help='Morphology statistics extractor, more details at' diff --git a/neurom/apps/config/morph_check.yaml b/neurom/apps/config/morph_check.yaml index 71e12cdc4..dc8048c86 100644 --- a/neurom/apps/config/morph_check.yaml +++ b/neurom/apps/config/morph_check.yaml @@ -12,7 +12,7 @@ color : true ############################################## checks: - neuron_checks: + morphology_checks: - has_basal_dendrite - has_axon - has_apical_dendrite diff --git a/neurom/apps/config/morph_stats.yaml b/neurom/apps/config/morph_stats.yaml index 49c5e5b6a..db63b4eba 100644 --- a/neurom/apps/config/morph_stats.yaml +++ b/neurom/apps/config/morph_stats.yaml @@ -7,9 +7,9 @@ neurite: section_lengths: - max - - total + - sum section_volumes: - - total + - sum section_branch_orders: - max @@ -19,8 +19,8 @@ neurite_type: - BASAL_DENDRITE - ALL -neuron: - soma_radii: +morphology: + soma_radius: - mean ############################################## diff --git a/neurom/apps/morph_stats.py b/neurom/apps/morph_stats.py index 2e2d39990..77680831e 100644 --- a/neurom/apps/morph_stats.py +++ b/neurom/apps/morph_stats.py @@ -32,10 +32,10 @@ import json import logging import multiprocessing -import numbers import os import warnings from collections import defaultdict +from collections.abc import Sized from functools import partial from itertools import chain, product from pathlib import Path @@ -47,11 +47,12 @@ import neurom as nm from neurom.apps import get_config -from neurom.core.neuron import Neuron +from neurom.core.morphology import Morphology from neurom.exceptions import ConfigError -from neurom.features import NEURITEFEATURES, NEURONFEATURES, _get_feature_value_and_func +from neurom.features import _NEURITE_FEATURES, _MORPHOLOGY_FEATURES, _POPULATION_FEATURES, \ + _get_feature_value_and_func from neurom.io.utils import get_files_by_path -from neurom.utils import NeuromJSON +from neurom.utils import NeuromJSON, warn_deprecated L = logging.getLogger(__name__) @@ -59,61 +60,28 @@ IGNORABLE_EXCEPTIONS = {'SomaError': SomaError} -def eval_stats(values, mode): - """Extract a summary statistic from an array of list of values. - - Arguments: - values: A numpy array of values - mode: A summary stat to extract. One of: - ['min', 'max', 'median', 'mean', 'std', 'raw', 'total'] - - .. note:: If values is empty, mode `raw` returns `[]`, `total` returns `0.0` - and the other modes return `None`. - """ - if mode == 'raw': - return values.tolist() - if mode == 'total': - mode = 'sum' - if len(values) == 0 and mode not in {'raw', 'sum'}: - return None - - return getattr(np, mode)(values, axis=0) - - -def _stat_name(feat_name, stat_mode): - """Set stat name based on feature name and stat mode.""" - if feat_name[-1] == 's': - feat_name = feat_name[:-1] - if feat_name == 'soma_radii': - feat_name = 'soma_radius' - if stat_mode == 'raw': - return feat_name - - return '%s_%s' % (stat_mode, feat_name) - - -def _run_extract_stats(nrn, config): +def _run_extract_stats(morph, config): """The function to be called by multiprocessing.Pool.imap_unordered.""" - if not isinstance(nrn, Neuron): - nrn = nm.load_neuron(nrn) - return nrn.name, extract_stats(nrn, config) + if not isinstance(morph, Morphology): + morph = nm.load_morphology(morph) + return morph.name, extract_stats(morph, config) -def extract_dataframe(neurons, config, n_workers=1): - """Extract stats grouped by neurite type from neurons. +def extract_dataframe(morphs, config, n_workers=1): + """Extract stats grouped by neurite type from morphs. Arguments: - neurons: a neuron, population, neurite tree or list of neuron paths + morphs: a morphology, population, neurite tree or list of morphology paths config (dict): configuration dict. The keys are: - neurite_type: a list of neurite types for which features are extracted If not provided, all neurite_type will be used - neurite: a dictionary {{neurite_feature: mode}} where: - neurite_feature is a string from NEURITEFEATURES or NEURONFEATURES - mode is an aggregation operation provided as a string such as: - ['min', 'max', 'median', 'mean', 'std', 'raw', 'total'] - - neuron: same as neurite entry, but it will not be run on each neurite_type, - but only once on the whole neuron. - n_workers (int): number of workers for multiprocessing (on collection of neurons) + ['min', 'max', 'median', 'mean', 'std', 'raw', 'sum'] + - morphology: same as neurite entry, but it will not be run on each neurite_type, + but only once on the whole morphology. + n_workers (int): number of workers for multiprocessing (on collection of morphs) Returns: The extracted statistics @@ -123,18 +91,18 @@ def extract_dataframe(neurons, config, n_workers=1): {config_path} """ - if isinstance(neurons, Neuron): - neurons = [neurons] + if isinstance(morphs, Morphology): + morphs = [morphs] config = config.copy() func = partial(_run_extract_stats, config=config) if n_workers == 1: - stats = list(map(func, neurons)) + stats = list(map(func, morphs)) else: if n_workers > os.cpu_count(): warnings.warn(f'n_workers ({n_workers}) > os.cpu_count() ({os.cpu_count()}))') with multiprocessing.Pool(n_workers) as pool: - stats = list(pool.imap(func, neurons)) + stats = list(pool.imap(func, morphs)) columns = [('property', 'name')] + [ (key1, key2) for key1, data in stats[0][1].items() for key2 in data @@ -147,20 +115,49 @@ def extract_dataframe(neurons, config, n_workers=1): extract_dataframe.__doc__ = extract_dataframe.__doc__.format(config_path=EXAMPLE_CONFIG) -def extract_stats(neurons, config): - """Extract stats from neurons. +def _get_feature_stats(feature_name, morphs, modes, kwargs): + """Insert the stat data in the dict. + + If the feature is 2-dimensional, the feature is flattened on its last axis + """ + data = {} + value, func = _get_feature_value_and_func(feature_name, morphs, **kwargs) + shape = func.shape + if len(shape) > 2: + raise ValueError(f'Len of "{feature_name}" feature shape must be <= 2') # pragma: no cover + + for mode in modes: + stat_name = f'{mode}_{feature_name}' + + stat = value + if isinstance(value, Sized): + if len(value) == 0 and mode not in {'raw', 'sum'}: + stat = None + else: + stat = getattr(np, mode)(value, axis=0) + + if len(shape) == 2: + for i in range(shape[1]): + data[f'{stat_name}_{i}'] = stat[i] if stat is not None else None + else: + data[stat_name] = stat + return data + + +def extract_stats(morphs, config): + """Extract stats from morphs. Arguments: - neurons: a neuron, population, neurite tree or list of neuron paths/str + morphs: a morphology, population, neurite tree or list of morphology paths/str config (dict): configuration dict. The keys are: - neurite_type: a list of neurite types for which features are extracted If not provided, all neurite_type will be used. - neurite: a dictionary {{neurite_feature: mode}} where: - neurite_feature is a string from NEURITEFEATURES or NEURONFEATURES - mode is an aggregation operation provided as a string such as: - ['min', 'max', 'median', 'mean', 'std', 'raw', 'total'] - - neuron: same as neurite entry, but it will not be run on each neurite_type, - but only once on the whole neuron. + ['min', 'max', 'median', 'mean', 'std', 'raw', 'sum'] + - morphology: same as neurite entry, but it will not be run on each neurite_type, + but only once on the whole morphology. Returns: The extracted statistics @@ -170,43 +167,37 @@ def extract_stats(neurons, config): {config_path} """ - - def _fill_stats_dict(data, stat_name, stat, shape): - """Insert the stat data in the dict. - - If the feature is 2-dimensional, the feature is flattened on its last axis - """ - if len(shape) == 2: - for i in range(shape[1]): - data[f'{stat_name}_{i}'] = stat[i] if stat is not None else None - elif len(shape) > 2: - raise ValueError(f'Feature with wrong shape: {shape}') # pragma: no cover - else: - data[stat_name] = stat - stats = defaultdict(dict) + neurite_features = product(['neurite'], config.get('neurite', {}).items()) + if 'neuron' in config: # pragma: no cover + warn_deprecated('Usage of "neuron" is deprecated in configs of `morph_stats` package. ' + 'Use "morphology" instead.') + config['morphology'] = config['neuron'] + del config['neuron'] + morph_features = product(['morphology'], config.get('morphology', {}).items()) + population_features = product(['population'], config.get('population', {}).items()) + neurite_types = [_NEURITE_MAP[t] for t in config.get('neurite_type', _NEURITE_MAP.keys())] + + for namespace, (feature_name, opts) in chain(neurite_features, morph_features, + population_features): + if isinstance(opts, dict): + kwargs = opts.get('kwargs', {}) + modes = opts.get('modes', []) + else: + kwargs = {} + modes = opts + if namespace == 'neurite': + if 'neurite_type' not in kwargs and neurite_types: + for t in neurite_types: + kwargs['neurite_type'] = t + stats[t.name].update(_get_feature_stats(feature_name, morphs, modes, kwargs)) + else: + t = _NEURITE_MAP[kwargs.get('neurite_type', 'ALL')] + kwargs['neurite_type'] = t + stats[t.name].update(_get_feature_stats(feature_name, morphs, modes, kwargs)) + else: + stats[namespace].update(_get_feature_stats(feature_name, morphs, modes, kwargs)) - for (feature_name, modes), neurite_type in product(config['neurite'].items(), - config.get('neurite_type', - _NEURITE_MAP.keys())): - neurite_type = _NEURITE_MAP[neurite_type] - feature, func = _get_feature_value_and_func(feature_name, neurons, - neurite_type=neurite_type) - if isinstance(feature, numbers.Number): - feature = [feature] - for mode in modes: - stat_name = _stat_name(feature_name, mode) - stat = eval_stats(feature, mode) - _fill_stats_dict(stats[neurite_type.name], stat_name, stat, func.shape) - - for feature_name, modes in config.get('neuron', {}).items(): - feature, func = _get_feature_value_and_func(feature_name, neurons) - if isinstance(feature, numbers.Number): - feature = [feature] - for mode in modes: - stat_name = _stat_name(feature_name, mode) - stat = eval_stats(feature, mode) - _fill_stats_dict(stats['neuron'], stat_name, stat, func.shape) return dict(stats) @@ -248,8 +239,9 @@ def full_config(): """Returns a config with all features, all modes, all neurite types.""" modes = ['min', 'max', 'median', 'mean', 'std'] return { - 'neurite': {feature: modes for feature in NEURITEFEATURES}, - 'neuron': {feature: modes for feature in NEURONFEATURES}, + 'neurite': {feature: modes for feature in _NEURITE_FEATURES}, + 'morphology': {feature: modes for feature in _MORPHOLOGY_FEATURES}, + 'population': {feature: modes for feature in _POPULATION_FEATURES}, 'neurite_type': list(_NEURITE_MAP.keys()), } @@ -262,8 +254,8 @@ def sanitize_config(config): else: config['neurite'] = {} - if 'neuron' not in config: - config['neuron'] = {} + if 'morphology' not in config: + config['morphology'] = {} return config @@ -292,14 +284,15 @@ def main(datapath, config, output_file, is_full_config, as_population, ignored_e if ignored_exceptions is None: ignored_exceptions = () ignored_exceptions = tuple(IGNORABLE_EXCEPTIONS[k] for k in ignored_exceptions) - neurons = nm.load_neurons(get_files_by_path(datapath), ignored_exceptions=ignored_exceptions) + morphs = nm.load_morphologies(get_files_by_path(datapath), + ignored_exceptions=ignored_exceptions) results = {} if as_population: - results[datapath] = extract_stats(neurons, config) + results[datapath] = extract_stats(morphs, config) else: - for neuron in tqdm(neurons): - results[neuron.name] = extract_stats(neuron, config) + for m in tqdm(morphs): + results[m.name] = extract_stats(m, config) if not output_file: print(json.dumps(results, indent=2, separators=(',', ':'), cls=NeuromJSON)) diff --git a/neurom/check/__init__.py b/neurom/check/__init__.py index 6259e2b50..0776510a2 100644 --- a/neurom/check/__init__.py +++ b/neurom/check/__init__.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Basic tools to check neuronal morphologies.""" +"""Basic tools to check morphologies.""" from functools import wraps diff --git a/neurom/check/morphology_checks.py b/neurom/check/morphology_checks.py new file mode 100644 index 000000000..5aa42bb7a --- /dev/null +++ b/neurom/check/morphology_checks.py @@ -0,0 +1,346 @@ +# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""NeuroM morphology checking functions. + +Contains functions for checking validity of morphology neurites and somata. +""" +from itertools import chain, islice + +import numpy as np +from neurom import NeuriteType +from neurom.check import CheckResult +from neurom.check.morphtree import get_flat_neurites +from neurom.core.morphology import Section, iter_neurites, iter_sections, iter_segments +from neurom.core.dataformat import COLS +from neurom.exceptions import NeuroMError +from neurom.morphmath import section_length, segment_length + + +def _read_neurite_type(neurite): + """Simply read the stored neurite type.""" + return neurite.type + + +def has_axon(morph, treefun=_read_neurite_type): + """Check if a morphology has an axon. + + Arguments: + morph(Morphology): The morphology object to test + treefun: Optional function to calculate the tree type of morphology's neurites + + Returns: + CheckResult with result + """ + return CheckResult(NeuriteType.axon in (treefun(n) for n in morph.neurites)) + + +def has_apical_dendrite(morph, min_number=1, treefun=_read_neurite_type): + """Check if a morphology has apical dendrites. + + Arguments: + morph(Morphology): the morphology to test + min_number: minimum number of apical dendrites required + treefun: optional function to calculate the tree type of morphology's neurites + + Returns: + CheckResult with result + """ + types = [treefun(n) for n in morph.neurites] + return CheckResult(types.count(NeuriteType.apical_dendrite) >= min_number) + + +def has_basal_dendrite(morph, min_number=1, treefun=_read_neurite_type): + """Check if a morphology has basal dendrites. + + Arguments: + morph(Morphology): The morphology object to test + min_number: minimum number of basal dendrites required + treefun: Optional function to calculate the tree type of morphology's neurites + + Returns: + CheckResult with result + """ + types = [treefun(n) for n in morph.neurites] + return CheckResult(types.count(NeuriteType.basal_dendrite) >= min_number) + + +def has_no_flat_neurites(morph, tol=0.1, method='ratio'): + """Check that a morphology has no flat neurites. + + Arguments: + morph(Morphology): The morphology object to test + tol(float): tolerance + method(string): way of determining flatness, 'tolerance', 'ratio' \ + as described in :meth:`neurom.check.morphtree.get_flat_neurites` + + Returns: + CheckResult with result + """ + return CheckResult(len(get_flat_neurites(morph, tol, method)) == 0) + + +def has_all_nonzero_segment_lengths(morph, threshold=0.0): + """Check presence of morphology segments with length not above threshold. + + Arguments: + morph(Morphology): the morphology to test + threshold(float): value above which a segment length is considered to be non-zero + + Returns: + CheckResult with result including list of (section_id, segment_id) + of zero length segments + """ + bad_ids = [] + for sec in iter_sections(morph): + p = sec.points + for i, s in enumerate(zip(p[:-1], p[1:])): + if segment_length(s) <= threshold: + bad_ids.append((sec.id, i)) + + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_all_nonzero_section_lengths(morph, threshold=0.0): + """Check presence of morphology sections with length not above threshold. + + Arguments: + morph(Morphology): the morphology to test + threshold(float): value above which a section length is considered to be non-zero + + Returns: + CheckResult with result including list of ids of bad sections + """ + bad_ids = [s.id for s in iter_sections(morph.neurites) + if section_length(s.points) <= threshold] + + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_all_nonzero_neurite_radii(morph, threshold=0.0): + """Check presence of neurite points with radius not above threshold. + + Arguments: + morph(Morphology): the morphology to test + threshold(float): value above which a radius is considered to be non-zero + + Returns: + CheckResult with result including list of (section ID, point ID) pairs + of zero-radius points + """ + bad_ids = [] + seen_ids = set() + for s in iter_sections(morph): + for i, p in enumerate(s.points): + info = (s.id, i) + if p[COLS.R] <= threshold and info not in seen_ids: + seen_ids.add(info) + bad_ids.append(info) + + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_nonzero_soma_radius(morph, threshold=0.0): + """Check if soma radius not above threshold. + + Arguments: + morph(Morphology): the morphology to test + threshold: value above which the soma radius is considered to be non-zero + + Returns: + CheckResult with result + """ + return CheckResult(morph.soma.radius > threshold) + + +def has_no_jumps(morph, max_distance=30.0, axis='z'): + """Check if there are jumps (large movements in the `axis`). + + Arguments: + morph(Morphology): the morphology to test + max_distance(float): value above which consecutive z-values are + considered a jump + axis(str): one of x/y/z, which axis to check for jumps + + Returns: + CheckResult with result list of ids of bad sections + """ + bad_ids = [] + axis = {'x': COLS.X, 'y': COLS.Y, 'z': COLS.Z, }[axis.lower()] + for neurite in iter_neurites(morph): + section_segment = ((sec, seg) for sec in iter_sections(neurite) + for seg in iter_segments(sec)) + for sec, (p0, p1) in islice(section_segment, 1, None): # Skip neurite root segment + if max_distance < abs(p0[axis] - p1[axis]): + bad_ids.append((sec.id, [p0, p1])) + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_no_root_node_jumps(morph, radius_multiplier=2): + """Check that the neurites have no root node jumps. + + Their first point not should not be further than `radius_multiplier * soma radius` from the + soma center + """ + bad_ids = [] + for neurite in iter_neurites(morph): + p0 = neurite.root_node.points[0, COLS.XYZ] + distance = np.linalg.norm(p0 - morph.soma.center) + if distance > radius_multiplier * morph.soma.radius: + bad_ids.append((neurite.root_node.id, [p0])) + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_no_fat_ends(morph, multiple_of_mean=2.0, final_point_count=5): + """Check if leaf points are too large. + + Arguments: + morph(Morphology): the morphology to test + multiple_of_mean(float): how many times larger the final radius + has to be compared to the mean of the final points + final_point_count(int): how many points to include in the mean + + Returns: + CheckResult with result list of ids of bad sections + + Note: + A fat end is defined as a leaf segment whose last point is larger + by a factor of `multiple_of_mean` than the mean of the points in + `final_point_count` + """ + bad_ids = [] + for leaf in iter_sections(morph.neurites, iterator_type=Section.ileaf): + mean_radius = np.mean(leaf.points[1:][-final_point_count:, COLS.R]) + + if mean_radius * multiple_of_mean <= leaf.points[-1, COLS.R]: + bad_ids.append((leaf.id, leaf.points[-1:])) + + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_no_narrow_start(morph, frac=0.9): + """Check if neurites have a narrow start. + + Arguments: + morph(Morphology): the morphology to test + frac(float): Ratio that the second point must be smaller than the first + + Returns: + CheckResult with a list of all first segments of neurites with a narrow start + """ + bad_ids = [(neurite.root_node.id, neurite.root_node.points[np.newaxis, 1]) + for neurite in morph.neurites + if neurite.root_node.points[0][COLS.R] < frac * neurite.root_node.points[1][COLS.R]] + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_no_dangling_branch(morph): + """Check if the morphology has dangling neurites. + + Are considered dangling + + - dendrites whose first point is too far from the soma center + - axons whose first point is too far from the soma center AND from + any point belonging to a dendrite + + Arguments: + morph(Morphology): the morphology to test + + Returns: + CheckResult with a list of all first segments of dangling neurites + """ + if len(morph.soma.points) == 0: + raise NeuroMError("Can't check for dangling neurites if there is no soma") + soma_center = morph.soma.points[:, COLS.XYZ].mean(axis=0) + recentered_soma = morph.soma.points[:, COLS.XYZ] - soma_center + radius = np.linalg.norm(recentered_soma, axis=1) + soma_max_radius = radius.max() + + dendritic_points = np.array(list(chain.from_iterable(n.points + for n in iter_neurites(morph) + if n.type != NeuriteType.axon))) + + def is_dangling(neurite): + """Is the neurite dangling?""" + starting_point = neurite.points[0][COLS.XYZ] + + if np.linalg.norm(starting_point - soma_center) - soma_max_radius <= 12.: + return False + + if neurite.type != NeuriteType.axon: + return True + + distance_to_dendrites = np.linalg.norm(dendritic_points[:, COLS.XYZ] - starting_point, + axis=1) + return np.all(distance_to_dendrites >= 2 * dendritic_points[:, COLS.R] + 2) + + bad_ids = [(n.root_node.id, [n.root_node.points[0]]) + for n in iter_neurites(morph) if is_dangling(n)] + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_no_narrow_neurite_section(morph, + neurite_filter, + radius_threshold=0.05, + considered_section_min_length=50): + """Check if the morphology has dendrites with narrow sections. + + Arguments: + morph(Morphology): the morphology to test + neurite_filter(callable): filter the neurites by this callable + radius_threshold(float): radii below this are considered narro + considered_section_min_length(float): sections with length below + this are not taken into account + + Returns: + CheckResult with result. `result.info` contains the narrow section ids and their + first point + """ + considered_sections = (sec for sec in iter_sections(morph, neurite_filter=neurite_filter) + if sec.length > considered_section_min_length) + + def narrow_section(section): + """Select narrow sections.""" + return section.points[:, COLS.R].mean() < radius_threshold + + bad_ids = [(section.id, section.points[np.newaxis, 1]) + for section in considered_sections if narrow_section(section)] + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_multifurcation(morph): + """Check if a section has more than 3 children.""" + bad_ids = [(section.id, section.points[np.newaxis, -1]) for section in iter_sections(morph) + if len(section.children) > 3] + return CheckResult(len(bad_ids) == 0, bad_ids) + + +def has_no_single_children(morph): + """Check if the morphology has sections with only one child section.""" + bad_ids = [section.id for section in iter_sections(morph) if len(section.children) == 1] + return CheckResult(len(bad_ids) == 0, bad_ids) diff --git a/neurom/check/morphtree.py b/neurom/check/morphtree.py index 6325e9ace..531715e7b 100644 --- a/neurom/check/morphtree.py +++ b/neurom/check/morphtree.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Python module of NeuroM to check neuronal trees.""" +"""Python module of NeuroM to check morphology trees.""" import numpy as np from neurom.core.dataformat import COLS @@ -170,50 +170,50 @@ def is_inside_cylinder(seg1, seg2): return not is_in_the_same_verse(seg1, seg2) and is_seg1_overlapping_with_seg2(seg1, seg2) # filter out single segment sections - section_itr = (snode for snode in neurite.iter_sections() if snode.points.shape[0] > 2) - for snode in section_itr: + section_itr = (sec for sec in neurite.iter_sections() if sec.points.shape[0] > 2) + for sec in section_itr: # group each section's points intro triplets - segment_pairs = list(filter(is_not_zero_seg, pair(snode.points))) + segment_pairs = list(filter(is_not_zero_seg, pair(sec.points))) # filter out zero length segments for i, seg1 in enumerate(segment_pairs[1:]): # check if the end point of the segment lies within the previous - # ones in the current sectionmake + # ones in the current section for seg2 in segment_pairs[0: i + 1]: if is_inside_cylinder(seg1, seg2): return True return False -def get_flat_neurites(neuron, tol=0.1, method='ratio'): - """Check if a neuron has neurites that are flat within a tolerance. +def get_flat_neurites(morph, tol=0.1, method='ratio'): + """Check if a morphology has neurites that are flat within a tolerance. Args: - neurite(Neurite): neurite to operate on + morph(Morphology): morphology to operate on tol(float): the tolerance or the ratio method(string): 'tolerance' or 'ratio' described in :meth:`is_flat` Returns: Bool list corresponding to the flatness check for each neurite - in neuron neurites with respect to the given criteria + in morphology neurites with respect to the given criteria """ - return [n for n in neuron.neurites if is_flat(n, tol, method)] + return [n for n in morph.neurites if is_flat(n, tol, method)] -def get_nonmonotonic_neurites(neuron, tol=1e-6): +def get_nonmonotonic_neurites(morph, tol=1e-6): """Get neurites that are not monotonic. Args: - neuron(Neuron): neuron to operate on + morph(Morphology): morphology to operate on tol(float): the tolerance or the ratio Returns: list of neurites that do not satisfy monotonicity test """ - return [n for n in neuron.neurites if not is_monotonic(n, tol)] + return [n for n in morph.neurites if not is_monotonic(n, tol)] -def get_back_tracking_neurites(neuron): +def get_back_tracking_neurites(morph): """Get neurites that have back-tracks. A back-track is the placement of a point near a previous segment during @@ -221,9 +221,9 @@ def get_back_tracking_neurites(neuron): cause issues with meshing algorithms. Args: - neuron(Neuron): neurite to operate on + morph(Morphology): neurite to operate on Returns: - List of neurons with backtracks + List of morphologies with backtracks """ - return [n for n in neuron.neurites if is_back_tracking(n)] + return [n for n in morph.neurites if is_back_tracking(n)] diff --git a/neurom/check/neuron_checks.py b/neurom/check/neuron_checks.py index 649c4825c..6146637a7 100644 --- a/neurom/check/neuron_checks.py +++ b/neurom/check/neuron_checks.py @@ -1,351 +1,8 @@ -# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""For backward compatibility only.""" +# pylint: skip-file -"""NeuroM neuron checking functions. +from neurom.check.morphology_checks import * # pragma: no cover +from neurom.utils import deprecated_module # pragma: no cover -Contains functions for checking validity of neuron neurites and somata. -""" -from itertools import chain, islice - -import numpy as np -from neurom import NeuriteType -from neurom.check import CheckResult -from neurom.check.morphtree import get_flat_neurites -from neurom.core.neuron import Section, iter_neurites, iter_sections, iter_segments -from neurom.core.dataformat import COLS -from neurom.exceptions import NeuroMError -from neurom.features import neuritefunc as _nf -from neurom.morphmath import section_length, segment_length - - -def _read_neurite_type(neurite): - """Simply read the stored neurite type.""" - return neurite.type - - -def has_axon(neuron, treefun=_read_neurite_type): - """Check if a neuron has an axon. - - Arguments: - neuron(Neuron): The neuron object to test - treefun: Optional function to calculate the tree type of - neuron's neurites - - Returns: - CheckResult with result - """ - return CheckResult(NeuriteType.axon in (treefun(n) for n in neuron.neurites)) - - -def has_apical_dendrite(neuron, min_number=1, treefun=_read_neurite_type): - """Check if a neuron has apical dendrites. - - Arguments: - neuron(Neuron): The neuron object to test - min_number: minimum number of apical dendrites required - treefun: Optional function to calculate the tree type of neuron's neurites - - Returns: - CheckResult with result - """ - types = [treefun(n) for n in neuron.neurites] - return CheckResult(types.count(NeuriteType.apical_dendrite) >= min_number) - - -def has_basal_dendrite(neuron, min_number=1, treefun=_read_neurite_type): - """Check if a neuron has basal dendrites. - - Arguments: - neuron(Neuron): The neuron object to test - min_number: minimum number of basal dendrites required - treefun: Optional function to calculate the tree type of neuron's - neurites - - Returns: - CheckResult with result - """ - types = [treefun(n) for n in neuron.neurites] - return CheckResult(types.count(NeuriteType.basal_dendrite) >= min_number) - - -def has_no_flat_neurites(neuron, tol=0.1, method='ratio'): - """Check that a neuron has no flat neurites. - - Arguments: - neuron(Neuron): The neuron object to test - tol(float): tolerance - method(string): way of determining flatness, 'tolerance', 'ratio' \ - as described in :meth:`neurom.check.morphtree.get_flat_neurites` - - Returns: - CheckResult with result - """ - return CheckResult(len(get_flat_neurites(neuron, tol, method)) == 0) - - -def has_all_nonzero_segment_lengths(neuron, threshold=0.0): - """Check presence of neuron segments with length not above threshold. - - Arguments: - neuron(Neuron): The neuron object to test - threshold(float): value above which a segment length is considered to - be non-zero - - Returns: - CheckResult with result including list of (section_id, segment_id) - of zero length segments - """ - bad_ids = [] - for sec in _nf.iter_sections(neuron): - p = sec.points - for i, s in enumerate(zip(p[:-1], p[1:])): - if segment_length(s) <= threshold: - bad_ids.append((sec.id, i)) - - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_all_nonzero_section_lengths(neuron, threshold=0.0): - """Check presence of neuron sections with length not above threshold. - - Arguments: - neuron(Neuron): The neuron object to test - threshold(float): value above which a section length is considered - to be non-zero - - Returns: - CheckResult with result including list of ids of bad sections - """ - bad_ids = [s.id for s in _nf.iter_sections(neuron.neurites) - if section_length(s.points) <= threshold] - - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_all_nonzero_neurite_radii(neuron, threshold=0.0): - """Check presence of neurite points with radius not above threshold. - - Arguments: - neuron(Neuron): The neuron object to test - threshold: value above which a radius is considered to be non-zero - - Returns: - CheckResult with result including list of (section ID, point ID) pairs - of zero-radius points - """ - bad_ids = [] - seen_ids = set() - for s in _nf.iter_sections(neuron): - for i, p in enumerate(s.points): - info = (s.id, i) - if p[COLS.R] <= threshold and info not in seen_ids: - seen_ids.add(info) - bad_ids.append(info) - - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_nonzero_soma_radius(neuron, threshold=0.0): - """Check if soma radius not above threshold. - - Arguments: - neuron(Neuron): The neuron object to test - threshold: value above which the soma radius is considered to be non-zero - - Returns: - CheckResult with result - """ - return CheckResult(neuron.soma.radius > threshold) - - -def has_no_jumps(neuron, max_distance=30.0, axis='z'): - """Check if there are jumps (large movements in the `axis`). - - Arguments: - neuron(Neuron): The neuron object to test - max_distance(float): value above which consecutive z-values are - considered a jump - axis(str): one of x/y/z, which axis to check for jumps - - Returns: - CheckResult with result list of ids of bad sections - """ - bad_ids = [] - axis = {'x': COLS.X, 'y': COLS.Y, 'z': COLS.Z, }[axis.lower()] - for neurite in iter_neurites(neuron): - section_segment = ((sec, seg) for sec in iter_sections(neurite) - for seg in iter_segments(sec)) - for sec, (p0, p1) in islice(section_segment, 1, None): # Skip neurite root segment - if max_distance < abs(p0[axis] - p1[axis]): - bad_ids.append((sec.id, [p0, p1])) - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_no_root_node_jumps(neuron, radius_multiplier=2): - """Check that the neurites have no root node jumps. - - Their first point not should not be further than `radius_multiplier * soma radius` from the - soma center - """ - bad_ids = [] - for neurite in iter_neurites(neuron): - p0 = neurite.root_node.points[0, COLS.XYZ] - distance = np.linalg.norm(p0 - neuron.soma.center) - if distance > radius_multiplier * neuron.soma.radius: - bad_ids.append((neurite.root_node.id, [p0])) - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_no_fat_ends(neuron, multiple_of_mean=2.0, final_point_count=5): - """Check if leaf points are too large. - - Arguments: - neuron(Neuron): The neuron object to test - multiple_of_mean(float): how many times larger the final radius - has to be compared to the mean of the final points - final_point_count(int): how many points to include in the mean - - Returns: - CheckResult with result list of ids of bad sections - - Note: - A fat end is defined as a leaf segment whose last point is larger - by a factor of `multiple_of_mean` than the mean of the points in - `final_point_count` - """ - bad_ids = [] - for leaf in _nf.iter_sections(neuron.neurites, iterator_type=Section.ileaf): - mean_radius = np.mean(leaf.points[1:][-final_point_count:, COLS.R]) - - if mean_radius * multiple_of_mean <= leaf.points[-1, COLS.R]: - bad_ids.append((leaf.id, leaf.points[-1:])) - - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_no_narrow_start(neuron, frac=0.9): - """Check if neurites have a narrow start. - - Arguments: - neuron(Neuron): The neuron object to test - frac(float): Ratio that the second point must be smaller than the first - - Returns: - CheckResult with a list of all first segments of neurites with a narrow start - """ - bad_ids = [(neurite.root_node.id, neurite.root_node.points[np.newaxis, 1]) - for neurite in neuron.neurites - if neurite.root_node.points[0][COLS.R] < frac * neurite.root_node.points[1][COLS.R]] - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_no_dangling_branch(neuron): - """Check if the neuron has dangling neurites. - - Are considered dangling - - - dendrites whose first point is too far from the soma center - - axons whose first point is too far from the soma center AND from - any point belonging to a dendrite - - Arguments: - neuron(Neuron): The neuron object to test - - Returns: - CheckResult with a list of all first segments of dangling neurites - """ - if len(neuron.soma.points) == 0: - raise NeuroMError('Can\'t check for dangling neurites if there is no soma') - soma_center = neuron.soma.points[:, COLS.XYZ].mean(axis=0) - recentered_soma = neuron.soma.points[:, COLS.XYZ] - soma_center - radius = np.linalg.norm(recentered_soma, axis=1) - soma_max_radius = radius.max() - - dendritic_points = np.array(list(chain.from_iterable(n.points - for n in iter_neurites(neuron) - if n.type != NeuriteType.axon))) - - def is_dangling(neurite): - """Is the neurite dangling ?.""" - starting_point = neurite.points[0][COLS.XYZ] - - if np.linalg.norm(starting_point - soma_center) - soma_max_radius <= 12.: - return False - - if neurite.type != NeuriteType.axon: - return True - - distance_to_dendrites = np.linalg.norm(dendritic_points[:, COLS.XYZ] - starting_point, - axis=1) - return np.all(distance_to_dendrites >= 2 * dendritic_points[:, COLS.R] + 2) - - bad_ids = [(n.root_node.id, [n.root_node.points[0]]) - for n in iter_neurites(neuron) if is_dangling(n)] - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_no_narrow_neurite_section(neuron, - neurite_filter, - radius_threshold=0.05, - considered_section_min_length=50): - """Check if the neuron has dendrites with narrow sections. - - Arguments: - neuron(Neuron): The neuron object to test - neurite_filter(callable): filter the neurites by this callable - radius_threshold(float): radii below this are considered narro - considered_section_min_length(float): sections with length below - this are not taken into account - - Returns: - CheckResult with result. result.info contains the narrow section ids and their - first point - """ - considered_sections = (sec for sec in iter_sections(neuron, neurite_filter=neurite_filter) - if sec.length > considered_section_min_length) - - def narrow_section(section): - """Select narrow sections.""" - return section.points[:, COLS.R].mean() < radius_threshold - - bad_ids = [(section.id, section.points[np.newaxis, 1]) - for section in considered_sections if narrow_section(section)] - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_multifurcation(neuron): - """Check if a section has more than 3 children.""" - bad_ids = [(section.id, section.points[np.newaxis, -1]) for section in iter_sections(neuron) - if len(section.children) > 3] - return CheckResult(len(bad_ids) == 0, bad_ids) - - -def has_no_single_children(neuron): - """Check if the neuron has sections with only one child section.""" - bad_ids = [section.id for section in iter_sections(neuron) if len(section.children) == 1] - return CheckResult(len(bad_ids) == 0, bad_ids) +deprecated_module('Module `neurom.check.neuron_checks` is deprecated. Use' + '`neurom.check.morphology_checks` instead.') # pragma: no cover diff --git a/neurom/check/runner.py b/neurom/check/runner.py index c104966e3..47dfbe5d6 100644 --- a/neurom/check/runner.py +++ b/neurom/check/runner.py @@ -27,16 +27,17 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Runner for neuron morphology checks.""" +"""Runner for morphology morphology checks.""" import logging from collections import OrderedDict from importlib import import_module -from neurom import load_neuron +from neurom import load_morphology from neurom.check import check_wrapper from neurom.exceptions import ConfigError from neurom.io import utils +from neurom.utils import warn_deprecated L = logging.getLogger(__name__) @@ -111,8 +112,8 @@ def _check_file(self, f): full_summary = OrderedDict() try: - nrn = load_neuron(f) - result, summary = self._check_loop(nrn, 'neuron_checks') + m = load_morphology(f) + result, summary = self._check_loop(m, 'morphology_checks') full_result &= result full_summary.update(summary) except Exception as e: # pylint: disable=W0703 @@ -144,8 +145,12 @@ def _sanitize_config(config): """Check that the config has the correct keys, add missing keys if necessary.""" if 'checks' in config: checks = config['checks'] - if 'neuron_checks' not in checks: - checks['neuron_checks'] = [] + if 'morphology_checks' not in checks: + checks['morphology_checks'] = [] + if 'neuron_checks' in checks: + warn_deprecated('"neuron_checks" is deprecated, use "morphology_checks" instead ' + 'for the config of `neurom.check`') # pragma: no cover + checks['morphology_checks'] = config['neuron_checks'] # pragma: no cover else: raise ConfigError('Need to have "checks" in the config') diff --git a/neurom/check/structural_checks.py b/neurom/check/structural_checks.py deleted file mode 100644 index 9a6ac12a3..000000000 --- a/neurom/check/structural_checks.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Module with consistency/validity checks for raw data blocks. Dropped in v2 version. - -These checks can't be implemented because MorphIO does not have access to raw underlying data -Also we can't rely on warnings from MorphIO. They can't be caught via `warnings.catch_warnings` -because its warnings are not Python warnings. -For example if we to implement `no_missing_parents` check via: - -try: - Neuron(neuron_file) -except MissingParentError as e: - return CheckResult(False, e) -return CheckResult(True) - -then this check will fail in case `neuron_file` is invalid due to any non parent error. For example -if `neuron_file` has invalid soma => an error is raised and this check fails. -""" - -raise NotImplementedError('Can not be implemented in v2 due to MorphIO constraints') diff --git a/neurom/core/__init__.py b/neurom/core/__init__.py index 40028fe69..2fddabf9d 100644 --- a/neurom/core/__init__.py +++ b/neurom/core/__init__.py @@ -30,5 +30,5 @@ # those imports here for backward compatibility from neurom.core.soma import Soma -from neurom.core.neuron import Section, Neurite, Neuron +from neurom.core.morphology import Section, Neurite, Morphology, Neuron from neurom.core.population import Population diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py new file mode 100644 index 000000000..077cb6b71 --- /dev/null +++ b/neurom/core/morphology.py @@ -0,0 +1,489 @@ +# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Morphology classes and functions.""" + +from collections import deque +from itertools import chain +import warnings + +import morphio +import numpy as np +from neurom import morphmath +from neurom.core.soma import make_soma +from neurom.core.dataformat import COLS +from neurom.core.types import NeuriteIter, NeuriteType +from neurom.core.population import Population +from neurom.utils import warn_deprecated + + +class Section: + """Simple recursive tree class.""" + + def __init__(self, morphio_section): + """The section constructor.""" + self.morphio_section = morphio_section + + @property + def id(self): + """Returns the section ID.""" + return self.morphio_section.id + + @property + def parent(self): + """Returns the parent section if non root section else None.""" + if self.morphio_section.is_root: + return None + return Section(self.morphio_section.parent) + + @property + def children(self): + """Returns a list of child section.""" + return [Section(child) for child in self.morphio_section.children] + + def append_section(self, section): + """Appends a section to the current section object. + + Args: + section (morphio.Section|morphio.mut.Section|Section|morphio.PointLevel): a section + """ + if isinstance(section, Section): + return self.morphio_section.append_section(section.morphio_section) + return self.morphio_section.append_section(section) + + def is_forking_point(self): + """Is this section a forking point?""" + return len(self.children) > 1 + + def is_bifurcation_point(self): + """Is tree a bifurcation point?""" + return len(self.children) == 2 + + def is_leaf(self): + """Is tree a leaf?""" + return len(self.children) == 0 + + def is_root(self): + """Is tree the root node?""" + return self.parent is None + + def ipreorder(self): + """Depth-first pre-order iteration of tree nodes.""" + children = deque((self, )) + while children: + cur_node = children.pop() + children.extend(reversed(cur_node.children)) + yield cur_node + + def ipostorder(self): + """Depth-first post-order iteration of tree nodes.""" + children = [self, ] + seen = set() + while children: + cur_node = children[-1] + if cur_node not in seen: + seen.add(cur_node) + children.extend(reversed(cur_node.children)) + else: + children.pop() + yield cur_node + + def iupstream(self): + """Iterate from a tree node to the root nodes.""" + t = self + while t is not None: + yield t + t = t.parent + + def ileaf(self): + """Iterator to all leaves of a tree.""" + return filter(Section.is_leaf, self.ipreorder()) + + def iforking_point(self, iter_mode=ipreorder): + """Iterator to forking points. + + Args: + iter_mode: iteration mode. Default: ipreorder. + """ + return filter(Section.is_forking_point, iter_mode(self)) + + def ibifurcation_point(self, iter_mode=ipreorder): + """Iterator to bifurcation points. + + Args: + iter_mode: iteration mode. Default: ipreorder. + """ + return filter(Section.is_bifurcation_point, iter_mode(self)) + + def __eq__(self, other): + """Equal when its morphio section is equal.""" + return self.morphio_section == other.morphio_section + + def __hash__(self): + """Hash of its id.""" + return self.id + + def __nonzero__(self): + """If has children.""" + return self.morphio_section is not None + + __bool__ = __nonzero__ + + @property + def points(self): + """Returns the section list of points the NeuroM way (points + radius).""" + return np.concatenate((self.morphio_section.points, + self.morphio_section.diameters[:, np.newaxis] / 2.), + axis=1) + + @points.setter + def points(self, value): + """Set the points.""" + self.morphio_section.points = np.copy(value[:, COLS.XYZ]) + self.morphio_section.diameters = np.copy(value[:, COLS.R]) * 2 + + @property + def type(self): + """Returns the section type.""" + return NeuriteType(int(self.morphio_section.type)) + + @property + def length(self): + """Return the path length of this section.""" + return morphmath.section_length(self.points) + + @property + def area(self): + """Return the surface area of this section. + + The area is calculated from the segments, as defined by this + section's points + """ + return sum(morphmath.segment_area(s) for s in iter_segments(self)) + + @property + def volume(self): + """Return the volume of this section. + + The volume is calculated from the segments, as defined by this + section's points + """ + return sum(morphmath.segment_volume(s) for s in iter_segments(self)) + + def __repr__(self): + """Text representation.""" + parent_id = None if self.parent is None else self.parent.id + return (f'Section(id={self.id}, type={self.type}, n_points={len(self.points)})' + f'') + + +# NRN simulator iteration order +# See: +# https://github.com/neuronsimulator/nrn/blob/2dbf2ebf95f1f8e5a9f0565272c18b1c87b2e54c/share/lib/hoc/import3d/import3d_gui.hoc#L874 +NRN_ORDER = {NeuriteType.soma: 0, + NeuriteType.axon: 1, + NeuriteType.basal_dendrite: 2, + NeuriteType.apical_dendrite: 3, + NeuriteType.undefined: 4} + + +def iter_neurites(obj, mapfun=None, filt=None, neurite_order=NeuriteIter.FileOrder): + """Iterator to a neurite, morphology or morphology population. + + Applies optional neurite filter and mapping functions. + + Arguments: + obj: a neurite, morphology or morphology population. + mapfun: optional neurite mapping function. + filt: optional neurite filter function. + neurite_order (NeuriteIter): order upon which neurites should be iterated + - NeuriteIter.FileOrder: order of appearance in the file + - NeuriteIter.NRN: NRN simulator order: soma -> axon -> basal -> apical + + Examples: + Get the number of points in each neurite in a morphology population + + >>> from neurom.core.morphology import iter_neurites + >>> from neurom import load_morphologies + >>> pop = load_morphologies('path/to/morphologies') + >>> n_points = [n for n in iter_neurites(pop, lambda x : len(x.points))] + + Get the number of points in each axon in a morphology population + + >>> import neurom as nm + >>> from neurom.core.morphology import iter_neurites + >>> filter = lambda n : n.type == nm.AXON + >>> mapping = lambda n : len(n.points) + >>> n_points = [n for n in iter_neurites(pop, mapping, filter)] + """ + neurites = ((obj,) if isinstance(obj, Neurite) else + obj.neurites if hasattr(obj, 'neurites') else obj) + if neurite_order == NeuriteIter.NRN: + if isinstance(obj, Population): + warnings.warn('`iter_neurites` with `neurite_order` over Population orders neurites' + 'within the whole population, not within each morphology separately.') + last_position = max(NRN_ORDER.values()) + 1 + neurites = sorted(neurites, key=lambda neurite: NRN_ORDER.get(neurite.type, last_position)) + + neurite_iter = iter(neurites) if filt is None else filter(filt, neurites) + return neurite_iter if mapfun is None else map(mapfun, neurite_iter) + + +def iter_sections(neurites, + iterator_type=Section.ipreorder, + neurite_filter=None, + neurite_order=NeuriteIter.FileOrder): + """Iterator to the sections in a neurite, morphology or morphology population. + + Arguments: + neurites: morphology, population, neurite, or iterable containing neurite objects + iterator_type: section iteration order within a given neurite. Must be one of: + Section.ipreorder: Depth-first pre-order iteration of tree nodes + Section.ipostorder: Depth-first post-order iteration of tree nodes + Section.iupstream: Iterate from a tree node to the root nodes + Section.ibifurcation_point: Iterator to bifurcation points + Section.ileaf: Iterator to all leaves of a tree + + neurite_filter: optional top level filter on properties of neurite neurite objects. + neurite_order (NeuriteIter): order upon which neurites should be iterated + - NeuriteIter.FileOrder: order of appearance in the file + - NeuriteIter.NRN: NRN simulator order: soma -> axon -> basal -> apical + + + Examples: + Get the number of points in each section of all the axons in a morphology population + + >>> import neurom as nm + >>> from neurom.core.morphology import iter_sections + >>> filter = lambda n : n.type == nm.AXON + >>> n_points = [len(s.points) for s in iter_sections(pop, neurite_filter=filter)] + """ + return chain.from_iterable( + iterator_type(neurite.root_node) for neurite in + iter_neurites(neurites, filt=neurite_filter, neurite_order=neurite_order)) + + +def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder): + """Return an iterator to the segments in a collection of neurites. + + Arguments: + obj: morphology, population, neurite, section, or iterable containing neurite objects + neurite_filter: optional top level filter on properties of neurite neurite objects + neurite_order: order upon which neurite should be iterated. Values: + - NeuriteIter.FileOrder: order of appearance in the file + - NeuriteIter.NRN: NRN simulator order: soma -> axon -> basal -> apical + + Note: + This is a convenience function provided for generic access to + morphology segments. It may have a performance overhead WRT custom-made + segment analysis functions that leverage numpy and section-wise iteration. + """ + sections = iter((obj,) if isinstance(obj, Section) else + iter_sections(obj, + neurite_filter=neurite_filter, + neurite_order=neurite_order)) + + return chain.from_iterable(zip(sec.points[:-1], sec.points[1:]) + for sec in sections) + + +def graft_morphology(section): + """Returns a morphology starting at section.""" + assert isinstance(section, Section) + m = morphio.mut.Morphology() + m.append_root_section(section.morphio_section) + return Morphology(m) + + +def graft_neuron(section): + """Deprecated in favor of ``graft_morphology``.""" + warn_deprecated('`neurom.core.neuron.graft_neuron` is deprecated in favor of ' + '`neurom.core.morphology.graft_morphology`') # pragma: no cover + return graft_morphology(section) # pragma: no cover + + +class Neurite: + """Class representing a neurite tree.""" + + def __init__(self, root_node): + """Constructor. + + Args: + root_node (morphio.Section): root section + """ + self.morphio_root_node = root_node + + @property + def root_node(self): + """The first section of the neurite.""" + return Section(self.morphio_root_node) + + @property + def type(self): + """The type of the root node.""" + return self.root_node.type + + @property + def points(self): + """Array with all the points in this neurite. + + Note: Duplicate points at section bifurcations are removed + """ + # Neurite first point must be added manually + _ptr = [self.root_node.points[0][COLS.XYZR]] + for s in iter_sections(self): + _ptr.append(s.points[1:, COLS.XYZR]) + return np.vstack(_ptr) + + @property + def length(self): + """Returns the total length of this neurite. + + The length is defined as the sum of lengths of the sections. + """ + return sum(s.length for s in self.iter_sections()) + + @property + def area(self): + """Return the surface area of this neurite. + + The area is defined as the sum of area of the sections. + """ + return sum(s.area for s in self.iter_sections()) + + @property + def volume(self): + """Return the volume of this neurite. + + The volume is defined as the sum of volumes of the sections. + """ + return sum(s.volume for s in self.iter_sections()) + + def iter_sections(self, order=Section.ipreorder, neurite_order=NeuriteIter.FileOrder): + """Iteration over section nodes. + + Arguments: + order: section iteration order within a given neurite. Must be one of: + Section.ipreorder: Depth-first pre-order iteration of tree nodes + Section.ipreorder: Depth-first post-order iteration of tree nodes + Section.iupstream: Iterate from a tree node to the root nodes + Section.ibifurcation_point: Iterator to bifurcation points + Section.ileaf: Iterator to all leaves of a tree + + neurite_order: order upon which neurites should be iterated. Values: + - NeuriteIter.FileOrder: order of appearance in the file + - NeuriteIter.NRN: NRN simulator order: soma -> axon -> basal -> apical + """ + return iter_sections(self, iterator_type=order, neurite_order=neurite_order) + + def __nonzero__(self): + """If has root node.""" + return bool(self.morphio_root_node) + + def __eq__(self, other): + """If root node ids and types are equal.""" + return self.type == other.type and self.morphio_root_node.id == other.morphio_root_node.id + + def __hash__(self): + """Hash is made of tuple of type and root_node.""" + return hash((self.type, self.root_node)) + + __bool__ = __nonzero__ + + def __repr__(self): + """Return a string representation.""" + return 'Neurite ' % self.type + + +class Morphology(morphio.mut.Morphology): + """Class representing a simple morphology.""" + + def __init__(self, filename, name=None): + """Morphology constructor. + + Args: + filename (str|Path): a filename + name (str): a option morphology name + """ + super().__init__(filename) + self.name = name if name else 'Morphology' + self.morphio_soma = super().soma + self.neurom_soma = make_soma(self.morphio_soma) + + @property + def soma(self): + """Corresponding soma.""" + return self.neurom_soma + + @property + def neurites(self): + """The list of neurites.""" + return [Neurite(root_section) for root_section in self.root_sections] + + @property + def sections(self): + """The array of all sections, excluding the soma.""" + return list(iter_sections(self)) + + @property + def points(self): + """Returns the list of points.""" + return np.concatenate( + [section.points for section in iter_sections(self)]) + + def transform(self, trans): + """Return a copy of this morphology with a 3D transformation applied.""" + obj = Morphology(self) + obj.morphio_soma.points = trans(obj.morphio_soma.points) + + for section in obj.sections: + section.morphio_section.points = trans(section.morphio_section.points) + return obj + + def __copy__(self): + """Creates a deep copy of Morphology instance.""" + return Morphology(self, self.name) + + def __deepcopy__(self, memodict={}): + """Creates a deep copy of Morphology instance.""" + # pylint: disable=dangerous-default-value + return Morphology(self, self.name) + + def __repr__(self): + """Return a string representation.""" + return 'Morphology ' % \ + (self.soma, len(self.neurites)) + + +class Neuron(Morphology): + """Deprecated ``Neuron`` class. Use ``Morphology`` instead.""" + def __init__(self, filename, name=None): + """Dont use me.""" + super().__init__(filename, name) # pragma: no cover + warn_deprecated('`neurom.core.neuron.Neuron` is deprecated in favor of ' + '`neurom.core.morphology.Morphology`') # pragma: no cover diff --git a/neurom/core/neuron.py b/neurom/core/neuron.py index 32155d12b..514e0eaf3 100644 --- a/neurom/core/neuron.py +++ b/neurom/core/neuron.py @@ -1,465 +1,8 @@ -# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""For backward compatibility only.""" +# pylint: skip-file -"""Neuron classes and functions.""" +from neurom.core.morphology import * # pragma: no cover +from neurom.utils import deprecated_module # pragma: no cover -from collections import deque -from itertools import chain - -import morphio -import numpy as np -from neurom import morphmath -from neurom.core.soma import make_soma -from neurom.core.dataformat import COLS -from neurom.core.types import NeuriteIter, NeuriteType - - -class Section: - """Simple recursive tree class.""" - - def __init__(self, morphio_section): - """The section constructor.""" - self.morphio_section = morphio_section - - @property - def id(self): - """Returns the section ID.""" - return self.morphio_section.id - - @property - def parent(self): - """Returns the parent section if non root section else None.""" - if self.morphio_section.is_root: - return None - return Section(self.morphio_section.parent) - - @property - def children(self): - """Returns a list of child section.""" - return [Section(child) for child in self.morphio_section.children] - - def append_section(self, section): - """Appends a section to the current section object. - - Args: - section (morphio.Section|morphio.mut.Section|Section|morphio.PointLevel): a section - """ - if isinstance(section, Section): - return self.morphio_section.append_section(section.morphio_section) - return self.morphio_section.append_section(section) - - def is_forking_point(self): - """Is this section a forking point?""" - return len(self.children) > 1 - - def is_bifurcation_point(self): - """Is tree a bifurcation point?""" - return len(self.children) == 2 - - def is_leaf(self): - """Is tree a leaf?""" - return len(self.children) == 0 - - def is_root(self): - """Is tree the root node?""" - return self.parent is None - - def ipreorder(self): - """Depth-first pre-order iteration of tree nodes.""" - children = deque((self, )) - while children: - cur_node = children.pop() - children.extend(reversed(cur_node.children)) - yield cur_node - - def ipostorder(self): - """Depth-first post-order iteration of tree nodes.""" - children = [self, ] - seen = set() - while children: - cur_node = children[-1] - if cur_node not in seen: - seen.add(cur_node) - children.extend(reversed(cur_node.children)) - else: - children.pop() - yield cur_node - - def iupstream(self): - """Iterate from a tree node to the root nodes.""" - t = self - while t is not None: - yield t - t = t.parent - - def ileaf(self): - """Iterator to all leaves of a tree.""" - return filter(Section.is_leaf, self.ipreorder()) - - def iforking_point(self, iter_mode=ipreorder): - """Iterator to forking points. - - Args: - iter_mode: iteration mode. Default: ipreorder. - """ - return filter(Section.is_forking_point, iter_mode(self)) - - def ibifurcation_point(self, iter_mode=ipreorder): - """Iterator to bifurcation points. - - Args: - iter_mode: iteration mode. Default: ipreorder. - """ - return filter(Section.is_bifurcation_point, iter_mode(self)) - - def __eq__(self, other): - """Equal when its morphio section is equal.""" - return self.morphio_section == other.morphio_section - - def __hash__(self): - """Hash of its id.""" - return self.id - - def __nonzero__(self): - """If has children.""" - return self.morphio_section is not None - - __bool__ = __nonzero__ - - @property - def points(self): - """Returns the section list of points the NeuroM way (points + radius).""" - return np.concatenate((self.morphio_section.points, - self.morphio_section.diameters[:, np.newaxis] / 2.), - axis=1) - - @points.setter - def points(self, value): - """Set the points.""" - self.morphio_section.points = np.copy(value[:, COLS.XYZ]) - self.morphio_section.diameters = np.copy(value[:, COLS.R]) * 2 - - @property - def type(self): - """Returns the section type.""" - return NeuriteType(int(self.morphio_section.type)) - - @property - def length(self): - """Return the path length of this section.""" - return morphmath.section_length(self.points) - - @property - def area(self): - """Return the surface area of this section. - - The area is calculated from the segments, as defined by this - section's points - """ - return sum(morphmath.segment_area(s) for s in iter_segments(self)) - - @property - def volume(self): - """Return the volume of this section. - - The volume is calculated from the segments, as defined by this - section's points - """ - return sum(morphmath.segment_volume(s) for s in iter_segments(self)) - - def __repr__(self): - """Text representation.""" - parent_id = None if self.parent is None else self.parent.id - return (f'Section(id={self.id}, type={self.type}, n_points={len(self.points)})' - f'') - - -# NRN simulator iteration order -# See: -# https://github.com/neuronsimulator/nrn/blob/2dbf2ebf95f1f8e5a9f0565272c18b1c87b2e54c/share/lib/hoc/import3d/import3d_gui.hoc#L874 -NRN_ORDER = {NeuriteType.soma: 0, - NeuriteType.axon: 1, - NeuriteType.basal_dendrite: 2, - NeuriteType.apical_dendrite: 3, - NeuriteType.undefined: 4} - - -def iter_neurites(obj, mapfun=None, filt=None, neurite_order=NeuriteIter.FileOrder): - """Iterator to a neurite, neuron or neuron population. - - Applies optional neurite filter and mapping functions. - - Arguments: - obj: a neurite, neuron or neuron population. - mapfun: optional neurite mapping function. - filt: optional neurite filter function. - neurite_order (NeuriteIter): order upon which neurites should be iterated - - NeuriteIter.FileOrder: order of appearance in the file - - NeuriteIter.NRN: NRN simulator order: soma -> axon -> basal -> apical - - Examples: - Get the number of points in each neurite in a neuron population - - >>> from neurom.core.neuron import iter_neurites - >>> n_points = [n for n in iter_neurites(pop, lambda x : len(x.points))] - - Get the number of points in each axon in a neuron population - - >>> import neurom as nm - >>> from neurom.core.neuron import iter_neurites - >>> filter = lambda n : n.type == nm.AXON - >>> mapping = lambda n : len(n.points) - >>> n_points = [n for n in iter_neurites(pop, mapping, filter)] - """ - neurites = ((obj,) if isinstance(obj, Neurite) else - obj.neurites if hasattr(obj, 'neurites') else obj) - if neurite_order == NeuriteIter.NRN: - last_position = max(NRN_ORDER.values()) + 1 - neurites = sorted(neurites, key=lambda neurite: NRN_ORDER.get(neurite.type, last_position)) - - neurite_iter = iter(neurites) if filt is None else filter(filt, neurites) - return neurite_iter if mapfun is None else map(mapfun, neurite_iter) - - -def iter_sections(neurites, - iterator_type=Section.ipreorder, - neurite_filter=None, - neurite_order=NeuriteIter.FileOrder): - """Iterator to the sections in a neurite, neuron or neuron population. - - Arguments: - neurites: neuron, population, neurite, or iterable containing neurite objects - iterator_type: section iteration order within a given neurite. Must be one of: - Section.ipreorder: Depth-first pre-order iteration of tree nodes - Section.ipostorder: Depth-first post-order iteration of tree nodes - Section.iupstream: Iterate from a tree node to the root nodes - Section.ibifurcation_point: Iterator to bifurcation points - Section.ileaf: Iterator to all leaves of a tree - - neurite_filter: optional top level filter on properties of neurite neurite objects. - neurite_order (NeuriteIter): order upon which neurites should be iterated - - NeuriteIter.FileOrder: order of appearance in the file - - NeuriteIter.NRN: NRN simulator order: soma -> axon -> basal -> apical - - - Examples: - Get the number of points in each section of all the axons in a neuron population - - >>> import neurom as nm - >>> from neurom.core.neuron import iter_sections - >>> filter = lambda n : n.type == nm.AXON - >>> n_points = [len(s.points) for s in iter_sections(pop, neurite_filter=filter)] - """ - return chain.from_iterable( - iterator_type(neurite.root_node) for neurite in - iter_neurites(neurites, filt=neurite_filter, neurite_order=neurite_order)) - - -def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder): - """Return an iterator to the segments in a collection of neurites. - - Arguments: - obj: neuron, population, neurite, section, or iterable containing neurite objects - neurite_filter: optional top level filter on properties of neurite neurite objects - neurite_order: order upon which neurite should be iterated. Values: - - NeuriteIter.FileOrder: order of appearance in the file - - NeuriteIter.NRN: NRN simulator order: soma -> axon -> basal -> apical - - Note: - This is a convenience function provided for generic access to - neuron segments. It may have a performance overhead WRT custom-made - segment analysis functions that leverage numpy and section-wise iteration. - """ - sections = iter((obj,) if isinstance(obj, Section) else - iter_sections(obj, - neurite_filter=neurite_filter, - neurite_order=neurite_order)) - - return chain.from_iterable(zip(sec.points[:-1], sec.points[1:]) - for sec in sections) - - -def graft_neuron(section): - """Returns a neuron starting at section.""" - assert isinstance(section, Section) - m = morphio.mut.Morphology() - m.append_root_section(section.morphio_section) - return Neuron(m) - - -class Neurite: - """Class representing a neurite tree.""" - - def __init__(self, root_node): - """Constructor. - - Args: - root_node (morphio.Section): root section - """ - self.morphio_root_node = root_node - - @property - def root_node(self): - """The first section of the neurite.""" - return Section(self.morphio_root_node) - - @property - def type(self): - """The type of the root node.""" - return self.root_node.type - - @property - def points(self): - """Array with all the points in this neurite. - - Note: Duplicate points at section bifurcations are removed - """ - # Neurite first point must be added manually - _ptr = [self.root_node.points[0][COLS.XYZR]] - for s in iter_sections(self): - _ptr.append(s.points[1:, COLS.XYZR]) - return np.vstack(_ptr) - - @property - def length(self): - """Returns the total length of this neurite. - - The length is defined as the sum of lengths of the sections. - """ - return sum(s.length for s in self.iter_sections()) - - @property - def area(self): - """Return the surface area of this neurite. - - The area is defined as the sum of area of the sections. - """ - return sum(s.area for s in self.iter_sections()) - - @property - def volume(self): - """Return the volume of this neurite. - - The volume is defined as the sum of volumes of the sections. - """ - return sum(s.volume for s in self.iter_sections()) - - def iter_sections(self, order=Section.ipreorder, neurite_order=NeuriteIter.FileOrder): - """Iteration over section nodes. - - Arguments: - order: section iteration order within a given neurite. Must be one of: - Section.ipreorder: Depth-first pre-order iteration of tree nodes - Section.ipreorder: Depth-first post-order iteration of tree nodes - Section.iupstream: Iterate from a tree node to the root nodes - Section.ibifurcation_point: Iterator to bifurcation points - Section.ileaf: Iterator to all leaves of a tree - - neurite_order: order upon which neurites should be iterated. Values: - - NeuriteIter.FileOrder: order of appearance in the file - - NeuriteIter.NRN: NRN simulator order: soma -> axon -> basal -> apical - """ - return iter_sections(self, iterator_type=order, neurite_order=neurite_order) - - def __nonzero__(self): - """If has root node.""" - return bool(self.morphio_root_node) - - def __eq__(self, other): - """If root node ids and types are equal.""" - return self.type == other.type and self.morphio_root_node.id == other.morphio_root_node.id - - def __hash__(self): - """Hash is made of tuple of type and root_node.""" - return hash((self.type, self.root_node)) - - __bool__ = __nonzero__ - - def __repr__(self): - """Return a string representation.""" - return 'Neurite ' % self.type - - -class Neuron(morphio.mut.Morphology): - """Class representing a simple neuron.""" - - def __init__(self, filename, name=None): - """Neuron constructor. - - Args: - filename (str|Path): a filename - name (str): a option neuron name - """ - super().__init__(filename) - self.name = name if name else 'Neuron' - self.morphio_soma = super().soma - self.neurom_soma = make_soma(self.morphio_soma) - - @property - def soma(self): - """Corresponding soma.""" - return self.neurom_soma - - @property - def neurites(self): - """The list of neurites.""" - return [Neurite(root_section) for root_section in self.root_sections] - - @property - def sections(self): - """The array of all sections, excluding the soma.""" - return list(iter_sections(self)) - - @property - def points(self): - """Returns the list of points.""" - return np.concatenate( - [section.points for section in iter_sections(self)]) - - def transform(self, trans): - """Return a copy of this neuron with a 3D transformation applied.""" - obj = Neuron(self) - obj.morphio_soma.points = trans(obj.morphio_soma.points) - - for section in obj.sections: - section.morphio_section.points = trans(section.morphio_section.points) - return obj - - def __copy__(self): - """Creates a deep copy of Neuron instance.""" - return Neuron(self, self.name) - - def __deepcopy__(self, memodict={}): - """Creates a deep copy of Neuron instance.""" - # pylint: disable=dangerous-default-value - return Neuron(self, self.name) - - def __repr__(self): - """Return a string representation.""" - return 'Neuron ' % \ - (self.soma, len(self.neurites)) +deprecated_module('Module `neurom.core.neuron` is deprecated. Use `neurom.core.morphology`' + ' instead.') # pragma: no cover diff --git a/neurom/core/population.py b/neurom/core/population.py index c464f1b26..fc29b53e4 100644 --- a/neurom/core/population.py +++ b/neurom/core/population.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Neuron Population Classes and Functions.""" +"""Morphology Population Classes and Functions.""" import logging from morphio import MorphioError @@ -38,24 +38,24 @@ class Population: - """Neuron Population Class. + """Morphology Population Class. - Offers an iterator over neurons within population, neurites of neurons, somas of neurons. - It does not store the loaded neuron in memory unless the neuron has been already passed - as loaded (instance of ``Neuron``). + Offers an iterator over morphs within population, neurites of morphs, somas of morphs. + It does not store the loaded morphology in memory unless the morphology has been already passed + as loaded (instance of ``Morphology``). """ def __init__(self, files, name='Population', ignored_exceptions=(), cache=False): - """Construct a neuron population. + """Construct a morphology population. Arguments: - files (collections.abc.Sequence[str|Path|Neuron]): collection of neuron files or - paths to them or instances of ``Neuron``. + files (collections.abc.Sequence[str|Path|Morphology]): collection of morphology files or + paths to them or instances of ``Morphology``. name (str): Optional name for this Population ignored_exceptions (tuple): NeuroM and MorphIO exceptions that you want to ignore when - loading neurons. - cache (bool): whether to cache the loaded neurons in memory. If false then a neuron + loading morphs. + cache (bool): whether to cache the loaded morphs in memory. If false then a morphology will be loaded everytime it is accessed within the population. Which is good when - population is big. If true then all neurons will be loaded upon the construction + population is big. If true then all morphs will be loaded upon the construction and kept in memory. """ self._ignored_exceptions = ignored_exceptions @@ -66,8 +66,8 @@ def __init__(self, files, name='Population', ignored_exceptions=(), cache=False) self._files = files @property - def neurons(self): - """Iterator to populations's somas.""" + def morphologies(self): + """Iterator to populations's morphologies.""" return (n for n in self) @property @@ -81,31 +81,31 @@ def neurites(self): return (neurite for n in self for neurite in n.neurites) def _load_file(self, f): - if isinstance(f, neurom.core.neuron.Neuron): + if isinstance(f, neurom.core.morphology.Morphology): return f try: - return neurom.load_neuron(f) + return neurom.load_morphology(f) except (NeuroMError, MorphioError) as e: if isinstance(e, self._ignored_exceptions): L.info('Ignoring exception "%s" for file %s', e, f.name) else: - raise NeuroMError('`load_neurons` failed') from e + raise NeuroMError('`load_morphologies` failed') from e return None def __iter__(self): - """Iterator to populations's neurons.""" + """Iterator to populations's morphs.""" for f in self._files: - nrn = self._load_file(f) - if nrn is None: + m = self._load_file(f) + if m is None: continue - yield nrn + yield m def __len__(self): - """Length of neuron collection.""" + """Length of morphology collection.""" return len(self._files) def __getitem__(self, idx): - """Get neuron at index idx.""" + """Get morphology at index idx.""" if idx > len(self): raise ValueError( f'no {idx} index in "{self.name}" population, max possible index is {len(self)}') @@ -113,4 +113,4 @@ def __getitem__(self, idx): def __str__(self): """Return a string representation.""" - return 'Population ' % (self.name, len(self)) + return f'Population ' diff --git a/neurom/core/tree.py b/neurom/core/tree.py deleted file mode 100644 index 34134d910..000000000 --- a/neurom/core/tree.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Tree class that is dropped in v2.""" - -raise NotImplementedError('This class is not accessible since v2 version. See documentation page' - '"Migration to v2 version".') diff --git a/neurom/core/types.py b/neurom/core/types.py index 503cf6640..922cc14e0 100644 --- a/neurom/core/types.py +++ b/neurom/core/types.py @@ -83,14 +83,14 @@ def tree_type_checker(*ref): Ex: >>> import neurom >>> from neurom.core.types import NeuriteType, tree_type_checker - >>> from neurom.core.neuron import Section - >>> nrn = neurom.load_neuron('path') + >>> from neurom.core.morphology import Section + >>> m = neurom.load_morphology('path') >>> >>> tree_filter = tree_type_checker(NeuriteType.axon, NeuriteType.basal_dendrite) - >>> nrn.i_neurites(Section.ipreorder, tree_filter=tree_filter) + >>> m.i_neurites(Section.ipreorder, tree_filter=tree_filter) >>> >>> tree_filter = tree_type_checker((NeuriteType.axon, NeuriteType.basal_dendrite)) - >>> nrn.i_neurites(Section.ipreorder, tree_filter=tree_filter) + >>> m.i_neurites(Section.ipreorder, tree_filter=tree_filter) """ ref = tuple(ref) if len(ref) == 1 and isinstance(ref[0], tuple): diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 8c2401be9..77a098c72 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -32,80 +32,115 @@ Obtain some morphometrics >>> import neurom >>> from neurom import features - >>> nrn = neurom.load_neuron('path/to/neuron') - >>> ap_seg_len = features.get('segment_lengths', nrn, neurite_type=neurom.APICAL_DENDRITE) - >>> ax_sec_len = features.get('section_lengths', nrn, neurite_type=neurom.AXON) + >>> m = neurom.load_morphology('path/to/morphology') + >>> ap_seg_len = features.get('segment_lengths', m, neurite_type=neurom.APICAL_DENDRITE) + >>> ax_sec_len = features.get('section_lengths', m, neurite_type=neurom.AXON) """ +import operator +from enum import Enum +from functools import reduce -import numpy as np - -from neurom.core.types import NeuriteType, tree_type_checker -from neurom.core.neuron import iter_neurites +from neurom.core import Population, Morphology, Neurite +from neurom.core.morphology import iter_neurites +from neurom.core.types import NeuriteType, tree_type_checker as is_type from neurom.exceptions import NeuroMError -from neurom.utils import deprecated - -NEURITEFEATURES = dict() -NEURONFEATURES = dict() - -@deprecated( - '`register_neurite_feature`', - 'Please use the decorator `neurom.features.register.feature` to register custom features') -def register_neurite_feature(name, func): - """Register a feature to be applied to neurites. +_NEURITE_FEATURES = {} +_MORPHOLOGY_FEATURES = {} +_POPULATION_FEATURES = {} - .. warning:: This feature has been deprecated in 1.6.0 - Arguments: - name: name of the feature, used for access via get() function. - func: single parameter function of a neurite. +class NameSpace(Enum): + """The level of morphology abstraction that feature applies to.""" + NEURITE = 'neurite' + NEURON = 'morphology' + POPULATION = 'population' - """ - def _fun(neurites, neurite_type=NeuriteType.all): - """Wrap neurite function from outer scope and map into list.""" - return list(func(n) for n in iter_neurites(neurites, filt=tree_type_checker(neurite_type))) - _register_feature('NEURITEFEATURES', name, _fun, shape=(...,)) +def _flatten_feature(feature_shape, feature_value): + """Flattens feature values. Applies for population features for backward compatibility.""" + if feature_shape == (): + return feature_value + return reduce(operator.concat, feature_value, []) -def _find_feature_func(feature_name): - """Returns the python function used when getting a feature with `neurom.get(feature_name)`.""" - for feature_dict in (NEURITEFEATURES, NEURONFEATURES): - if feature_name in feature_dict: - return feature_dict[feature_name] - raise NeuroMError(f'Unable to find feature: {feature_name}') +def _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs): + """Collects neurite feature values appropriately to feature's shape.""" + kwargs.pop('neurite_type', None) # there is no 'neurite_type' arg in _NEURITE_FEATURES + return reduce(operator.add, + (feature_(n, **kwargs) for n in iter_neurites(obj, filt=neurite_filter)), + 0 if feature_.shape == () else []) def _get_feature_value_and_func(feature_name, obj, **kwargs): """Obtain a feature from a set of morphology objects. Arguments: - feature(string): feature to extract - obj: a neuron, population or neurite tree + feature_name(string): feature to extract + obj (Neurite|Morphology|Population): neurite, morphology or population kwargs: parameters to forward to underlying worker functions Returns: A tuple (feature, func) of the feature value and its function """ - feat = _find_feature_func(feature_name) - - res = feat(obj, **kwargs) - if len(feat.shape) != 0: - res = np.array(list(res)) - - return res, feat + # pylint: disable=too-many-branches + is_obj_list = isinstance(obj, (list, tuple)) + if not isinstance(obj, (Neurite, Morphology, Population)) and not is_obj_list: + raise NeuroMError('Only Neurite, Morphology, Population or list, tuple of Neurite,' + ' Morphology can be used for feature calculation') + + neurite_filter = is_type(kwargs.get('neurite_type', NeuriteType.all)) + res, feature_ = None, None + + if isinstance(obj, Neurite) or (is_obj_list and isinstance(obj[0], Neurite)): + # input is a neurite or a list of neurites + if feature_name in _NEURITE_FEATURES: + assert 'neurite_type' not in kwargs, 'Cant apply "neurite_type" arg to a neurite with' \ + ' a neurite feature' + feature_ = _NEURITE_FEATURES[feature_name] + if isinstance(obj, Neurite): + res = feature_(obj, **kwargs) + else: + res = [feature_(s, **kwargs) for s in obj] + elif isinstance(obj, Morphology): + # input is a morphology + if feature_name in _MORPHOLOGY_FEATURES: + feature_ = _MORPHOLOGY_FEATURES[feature_name] + res = feature_(obj, **kwargs) + elif feature_name in _NEURITE_FEATURES: + feature_ = _NEURITE_FEATURES[feature_name] + res = _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs) + elif isinstance(obj, Population) or (is_obj_list and isinstance(obj[0], Morphology)): + # input is a morphology population or a list of morphs + if feature_name in _POPULATION_FEATURES: + feature_ = _POPULATION_FEATURES[feature_name] + res = feature_(obj, **kwargs) + elif feature_name in _MORPHOLOGY_FEATURES: + feature_ = _MORPHOLOGY_FEATURES[feature_name] + res = _flatten_feature(feature_.shape, [feature_(n, **kwargs) for n in obj]) + elif feature_name in _NEURITE_FEATURES: + feature_ = _NEURITE_FEATURES[feature_name] + res = _flatten_feature( + feature_.shape, + [_get_neurites_feature_value(feature_, n, neurite_filter, kwargs) for n in obj]) + + if res is None or feature_ is None: + raise NeuroMError(f'Cant apply "{feature_name}" feature. Please check that it exists, ' + 'and can be applied to your input. See the features documentation page.') + + return res, feature_ def get(feature_name, obj, **kwargs): """Obtain a feature from a set of morphology objects. - Features can be either Neurite features or Neuron features. For the list of Neurite features - see :mod:`neurom.features.neuritefunc`. For the list of Neuron features see - :mod:`neurom.features.neuronfunc`. + Features can be either Neurite, Morphology or Population features. For Neurite features see + :mod:`neurom.features.neurite`. For Morphology features see :mod:`neurom.features.morphology`. + For Population features see :mod:`neurom.features.population`. Arguments: feature_name(string): feature to extract - obj: a neuron, a neuron population or a neurite tree + obj: a morphology, a morphology population or a neurite tree kwargs: parameters to forward to underlying worker functions Returns: @@ -114,41 +149,43 @@ def get(feature_name, obj, **kwargs): return _get_feature_value_and_func(feature_name, obj, **kwargs)[0] -def _register_feature(namespace, name, func, shape): +def _register_feature(namespace: NameSpace, name, func, shape): """Register a feature to be applied. Upon registration, an attribute 'shape' containing the expected shape of the function return is added to 'func'. Arguments: - namespace(string): a namespace (must be 'NEURITEFEATURES' or 'NEURONFEATURES') + namespace(string): a namespace, see :class:`NameSpace` name(string): name of the feature, used to access the feature via `neurom.features.get()`. func(callable): single parameter function of a neurite. shape(tuple): the expected shape of the feature values """ setattr(func, 'shape', shape) + _map = {NameSpace.NEURITE: _NEURITE_FEATURES, + NameSpace.NEURON: _MORPHOLOGY_FEATURES, + NameSpace.POPULATION: _POPULATION_FEATURES} + if name in _map[namespace]: + raise NeuroMError(f'A feature is already registered under "{name}"') + _map[namespace][name] = func - assert namespace in {'NEURITEFEATURES', 'NEURONFEATURES'} - feature_dict = globals()[namespace] - - if name in feature_dict: - raise NeuroMError('Attempt to hide registered feature %s' % name) - feature_dict[name] = func - -def feature(shape, namespace=None, name=None): +def feature(shape, namespace: NameSpace, name=None): """Feature decorator to automatically register the feature in the appropriate namespace. Arguments: shape(tuple): the expected shape of the feature values - namespace(string): a namespace (must be 'NEURITEFEATURES' or 'NEURONFEATURES') + namespace(string): a namespace, see :class:`NameSpace` name(string): name of the feature, used to access the feature via `neurom.features.get()`. """ + def inner(func): _register_feature(namespace, name or func.__name__, func, shape) return func + return inner # These imports are necessary in order to register the features -from neurom.features import neuritefunc, neuronfunc # noqa, pylint: disable=wrong-import-position +from neurom.features import neurite, morphology, \ + population # noqa, pylint: disable=wrong-import-position diff --git a/neurom/features/bifurcation.py b/neurom/features/bifurcation.py new file mode 100644 index 000000000..65db001e9 --- /dev/null +++ b/neurom/features/bifurcation.py @@ -0,0 +1,186 @@ +# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Bifurcation point functions.""" + +import numpy as np +from neurom import morphmath +from neurom.exceptions import NeuroMError +from neurom.core.dataformat import COLS +from neurom.features.section import section_mean_radius + + +def _raise_if_not_bifurcation(section): + n_children = len(section.children) + if n_children != 2: + raise NeuroMError('A bifurcation point must have exactly 2 children, found {}'.format( + n_children)) + + +def local_bifurcation_angle(bif_point): + """Return the opening angle between two out-going sections in a bifurcation point. + + We first ensure that the input point has only two children. + + The bifurcation angle is defined as the angle between the first non-zero + length segments of a bifurcation point. + """ + def skip_0_length(sec): + """Return the first point with non-zero distance to first point.""" + p0 = sec[0] + cur = sec[1] + for i, p in enumerate(sec[1:]): + if not np.all(p[:COLS.R] == p0[:COLS.R]): + cur = sec[i + 1] + break + + return cur + + _raise_if_not_bifurcation(bif_point) + + ch0, ch1 = (skip_0_length(bif_point.children[0].points), + skip_0_length(bif_point.children[1].points)) + + return morphmath.angle_3points(bif_point.points[-1], ch0, ch1) + + +def remote_bifurcation_angle(bif_point): + """Return the opening angle between two out-going sections in a bifurcation point. + + We first ensure that the input point has only two children. + + The angle is defined as between the bifurcation point and the + last points in the out-going sections. + """ + _raise_if_not_bifurcation(bif_point) + + return morphmath.angle_3points(bif_point.points[-1], + bif_point.children[0].points[-1], + bif_point.children[1].points[-1]) + + +def bifurcation_partition(bif_point): + """Calculate the partition at a bifurcation point. + + We first ensure that the input point has only two children. + + The number of nodes in each child tree is counted. The partition is + defined as the ratio of the largest number to the smallest number. + """ + _raise_if_not_bifurcation(bif_point) + + n = float(sum(1 for _ in bif_point.children[0].ipreorder())) + m = float(sum(1 for _ in bif_point.children[1].ipreorder())) + return max(n, m) / min(n, m) + + +def partition_asymmetry(bif_point, uylings=False): + """Calculate the partition asymmetry at a bifurcation point. + + By default partition asymmetry is defined as in https://www.ncbi.nlm.nih.gov/pubmed/18568015. + However if ``uylings=True`` is set then + https://jvanpelt.nl/papers/Uylings_Network_13_2002_397-414.pdf is used. + + The number of nodes in each child tree is counted. The partition + is defined as the ratio of the absolute difference and the sum + of the number of bifurcations in the two child subtrees + at each branch point. + """ + _raise_if_not_bifurcation(bif_point) + + n = float(sum(1 for _ in bif_point.children[0].ipreorder())) + m = float(sum(1 for _ in bif_point.children[1].ipreorder())) + c = 0 + if uylings: + c = 2 + if n + m <= c: + raise NeuroMError('Partition asymmetry cant be calculated by Uylings because the sum of' + 'terminal tips is less than 2.') + return abs(n - m) / abs(n + m - c) + + +def partition_pair(bif_point): + """Calculate the partition pairs at a bifurcation point. + + The number of nodes in each child tree is counted. The partition + pairs is the number of bifurcations in the two child subtrees + at each branch point. + """ + n = float(sum(1 for _ in bif_point.children[0].ipreorder())) + m = float(sum(1 for _ in bif_point.children[1].ipreorder())) + return (n, m) + + +def sibling_ratio(bif_point, method='first'): + """Calculate the sibling ratio of a bifurcation point. + + The sibling ratio is the ratio between the diameters of the + smallest and the largest child. It is a real number between + 0 and 1. Method argument allows one to consider mean diameters + along the child section instead of diameter of the first point. + """ + _raise_if_not_bifurcation(bif_point) + + if method not in {'first', 'mean'}: + raise ValueError('Please provide a valid method for sibling ratio, found %s' % method) + + if method == 'first': + # the first point is the same as the parent last point + n = bif_point.children[0].points[1, COLS.R] + m = bif_point.children[1].points[1, COLS.R] + if method == 'mean': + n = section_mean_radius(bif_point.children[0]) + m = section_mean_radius(bif_point.children[1]) + return min(n, m) / max(n, m) + + +def diameter_power_relation(bif_point, method='first'): + """Calculate the diameter power relation at a bifurcation point. + + The diameter power relation is defined in https://www.ncbi.nlm.nih.gov/pubmed/18568015 + + This quantity gives an indication of how far the branching is from + the Rall ratio + + diameter_power_relation==1 means perfect Rall ratio + """ + _raise_if_not_bifurcation(bif_point) + + if method not in {'first', 'mean'}: + raise ValueError('Please provide a valid method for sibling ratio, found %s' % method) + + if method == 'first': + # the first point is the same as the parent last point + d_child = bif_point.points[-1, COLS.R] + d_child1 = bif_point.children[0].points[1, COLS.R] + d_child2 = bif_point.children[1].points[1, COLS.R] + if method == 'mean': + d_child = section_mean_radius(bif_point) + d_child1 = section_mean_radius(bif_point.children[0]) + d_child2 = section_mean_radius(bif_point.children[1]) + return (d_child / d_child1)**(1.5) + (d_child / d_child2)**(1.5) diff --git a/neurom/features/bifurcationfunc.py b/neurom/features/bifurcationfunc.py index 7c8333925..52490a2ca 100644 --- a/neurom/features/bifurcationfunc.py +++ b/neurom/features/bifurcationfunc.py @@ -1,186 +1,8 @@ -# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""For backward compatibility only.""" +# pylint: skip-file -"""Bifurcation point functions.""" +from neurom.features.bifurcation import * # pragma: no cover +from neurom.utils import deprecated_module # pragma: no cover -import numpy as np -from neurom import morphmath -from neurom.exceptions import NeuroMError -from neurom.core.dataformat import COLS -from neurom.features.sectionfunc import section_mean_radius - - -def _raise_if_not_bifurcation(section): - n_children = len(section.children) - if n_children != 2: - raise NeuroMError('A bifurcation point must have exactly 2 children, found {}'.format( - n_children)) - - -def local_bifurcation_angle(bif_point): - """Return the opening angle between two out-going sections in a bifurcation point. - - We first ensure that the input point has only two children. - - The bifurcation angle is defined as the angle between the first non-zero - length segments of a bifurcation point. - """ - def skip_0_length(sec): - """Return the first point with non-zero distance to first point.""" - p0 = sec[0] - cur = sec[1] - for i, p in enumerate(sec[1:]): - if not np.all(p[:COLS.R] == p0[:COLS.R]): - cur = sec[i + 1] - break - - return cur - - _raise_if_not_bifurcation(bif_point) - - ch0, ch1 = (skip_0_length(bif_point.children[0].points), - skip_0_length(bif_point.children[1].points)) - - return morphmath.angle_3points(bif_point.points[-1], ch0, ch1) - - -def remote_bifurcation_angle(bif_point): - """Return the opening angle between two out-going sections in a bifurcation point. - - We first ensure that the input point has only two children. - - The angle is defined as between the bifurcation point and the - last points in the out-going sections. - """ - _raise_if_not_bifurcation(bif_point) - - return morphmath.angle_3points(bif_point.points[-1], - bif_point.children[0].points[-1], - bif_point.children[1].points[-1]) - - -def bifurcation_partition(bif_point): - """Calculate the partition at a bifurcation point. - - We first ensure that the input point has only two children. - - The number of nodes in each child tree is counted. The partition is - defined as the ratio of the largest number to the smallest number. - """ - _raise_if_not_bifurcation(bif_point) - - n = float(sum(1 for _ in bif_point.children[0].ipreorder())) - m = float(sum(1 for _ in bif_point.children[1].ipreorder())) - return max(n, m) / min(n, m) - - -def partition_asymmetry(bif_point, uylings=False): - """Calculate the partition asymmetry at a bifurcation point. - - By default partition asymmetry is defined as in https://www.ncbi.nlm.nih.gov/pubmed/18568015. - However if ``uylings=True`` is set then - https://jvanpelt.nl/papers/Uylings_Network_13_2002_397-414.pdf is used. - - The number of nodes in each child tree is counted. The partition - is defined as the ratio of the absolute difference and the sum - of the number of bifurcations in the two child subtrees - at each branch point. - """ - _raise_if_not_bifurcation(bif_point) - - n = float(sum(1 for _ in bif_point.children[0].ipreorder())) - m = float(sum(1 for _ in bif_point.children[1].ipreorder())) - c = 0 - if uylings: - c = 2 - if n + m <= c: - raise NeuroMError('Partition asymmetry cant be calculated by Uylings because the sum of' - 'terminal tips is less than 2.') - return abs(n - m) / abs(n + m - c) - - -def partition_pair(bif_point): - """Calculate the partition pairs at a bifurcation point. - - The number of nodes in each child tree is counted. The partition - pairs is the number of bifurcations in the two child subtrees - at each branch point. - """ - n = float(sum(1 for _ in bif_point.children[0].ipreorder())) - m = float(sum(1 for _ in bif_point.children[1].ipreorder())) - return (n, m) - - -def sibling_ratio(bif_point, method='first'): - """Calculate the sibling ratio of a bifurcation point. - - The sibling ratio is the ratio between the diameters of the - smallest and the largest child. It is a real number between - 0 and 1. Method argument allows one to consider mean diameters - along the child section instead of diameter of the first point. - """ - _raise_if_not_bifurcation(bif_point) - - if method not in {'first', 'mean'}: - raise ValueError('Please provide a valid method for sibling ratio, found %s' % method) - - if method == 'first': - # the first point is the same as the parent last point - n = bif_point.children[0].points[1, COLS.R] - m = bif_point.children[1].points[1, COLS.R] - if method == 'mean': - n = section_mean_radius(bif_point.children[0]) - m = section_mean_radius(bif_point.children[1]) - return min(n, m) / max(n, m) - - -def diameter_power_relation(bif_point, method='first'): - """Calculate the diameter power relation at a bifurcation point. - - The diameter power relation is defined in https://www.ncbi.nlm.nih.gov/pubmed/18568015 - - This quantity gives an indication of how far the branching is from - the Rall ratio - - diameter_power_relation==1 means perfect Rall ratio - """ - _raise_if_not_bifurcation(bif_point) - - if method not in {'first', 'mean'}: - raise ValueError('Please provide a valid method for sibling ratio, found %s' % method) - - if method == 'first': - # the first point is the same as the parent last point - d_child = bif_point.points[-1, COLS.R] - d_child1 = bif_point.children[0].points[1, COLS.R] - d_child2 = bif_point.children[1].points[1, COLS.R] - if method == 'mean': - d_child = section_mean_radius(bif_point) - d_child1 = section_mean_radius(bif_point.children[0]) - d_child2 = section_mean_radius(bif_point.children[1]) - return (d_child / d_child1)**(1.5) + (d_child / d_child2)**(1.5) +deprecated_module('Module `neurom.features.bifurcationfunc` is deprecated. Use' + '`neurom.features.bifurcation` instead.') # pragma: no cover diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py new file mode 100644 index 000000000..39652e1e8 --- /dev/null +++ b/neurom/features/morphology.py @@ -0,0 +1,296 @@ +# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Morphology features. + +Any public function from this namespace can be called via the features mechanism. If calling +directly the function in this namespace can only accept a morphology as its input. If you want to +apply it to a morphology population then you must use the features mechanism e.g. ``features.get``. +The features mechanism does not allow you to apply these features to neurites. + +>>> import neurom +>>> from neurom import features +>>> m = neurom.load_morphology('path/to/morphology') +>>> features.get('soma_surface_area', m) +>>> population = neurom.load_morphologies('path/to/morphs') +>>> features.get('sholl_crossings', population) + +For more details see :ref:`features`. +""" + + +from functools import partial +import math +import numpy as np + +from neurom import morphmath +from neurom.core.morphology import iter_neurites, iter_segments, Morphology +from neurom.core.types import tree_type_checker as is_type +from neurom.core.dataformat import COLS +from neurom.core.types import NeuriteType +from neurom.features import feature, NameSpace, neurite as nf + +feature = partial(feature, namespace=NameSpace.NEURON) + + +@feature(shape=()) +def soma_volume(morph): + """Get the volume of a morphology's soma.""" + return morph.soma.volume + + +@feature(shape=()) +def soma_surface_area(morph): + """Get the surface area of a morphology's soma. + + Note: + The surface area is calculated by assuming the soma is spherical. + """ + return 4 * math.pi * morph.soma.radius ** 2 + + +@feature(shape=()) +def soma_radius(morph): + """Get the radius of a morphology's soma.""" + return morph.soma.radius + + +@feature(shape=()) +def max_radial_distance(morph, neurite_type=NeuriteType.all): + """Get the maximum radial distances of the termination sections.""" + term_radial_distances = [nf.max_radial_distance(n) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + return max(term_radial_distances) if term_radial_distances else 0. + + +@feature(shape=(...,)) +def number_of_sections_per_neurite(morph, neurite_type=NeuriteType.all): + """List of numbers of sections per neurite.""" + return [nf.number_of_sections(n) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=(...,)) +def total_length_per_neurite(morph, neurite_type=NeuriteType.all): + """Neurite lengths.""" + return [nf.total_length(n) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=(...,)) +def total_area_per_neurite(morph, neurite_type=NeuriteType.all): + """Neurite areas.""" + return [nf.total_area(n) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=(...,)) +def total_volume_per_neurite(morph, neurite_type=NeuriteType.all): + """Neurite volumes.""" + return [nf.total_volume(n) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=(...,)) +def trunk_origin_azimuths(morph, neurite_type=NeuriteType.all): + """Get a list of all the trunk origin azimuths of a morph. + + The azimuth is defined as Angle between x-axis and the vector + defined by (initial tree point - soma center) on the x-z plane. + + The range of the azimuth angle [-pi, pi] radians + """ + def _azimuth(section, soma): + """Azimuth of a section.""" + vector = morphmath.vector(section[0], soma.center) + return np.arctan2(vector[COLS.Z], vector[COLS.X]) + + return [_azimuth(n.root_node.points, morph.soma) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=(...,)) +def trunk_origin_elevations(morph, neurite_type=NeuriteType.all): + """Get a list of all the trunk origin elevations of a morph. + + The elevation is defined as the angle between x-axis and the + vector defined by (initial tree point - soma center) + on the x-y half-plane. + + The range of the elevation angle [-pi/2, pi/2] radians + """ + def _elevation(section, soma): + """Elevation of a section.""" + vector = morphmath.vector(section[0], soma.center) + norm_vector = np.linalg.norm(vector) + + if norm_vector >= np.finfo(type(norm_vector)).eps: + return np.arcsin(vector[COLS.Y] / norm_vector) + raise ValueError("Norm of vector between soma center and section is almost zero.") + + return [_elevation(n.root_node.points, morph.soma) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=(...,)) +def trunk_vectors(morph, neurite_type=NeuriteType.all): + """Calculate the vectors between all the trunks of the morphology and the soma center.""" + return [morphmath.vector(n.root_node.points[0], morph.soma.center) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=(...,)) +def trunk_angles(morph, neurite_type=NeuriteType.all): + """Calculate the angles between all the trunks of the morph. + + The angles are defined on the x-y plane and the trees + are sorted from the y axis and anticlock-wise. + """ + vectors = np.array(trunk_vectors(morph, neurite_type=neurite_type)) + # In order to avoid the failure of the process in case the neurite_type does not exist + if len(vectors) == 0: + return [] + + def _sort_angle(p1, p2): + """Angle between p1-p2 to sort vectors.""" + ang1 = np.arctan2(*p1[::-1]) + ang2 = np.arctan2(*p2[::-1]) + return ang1 - ang2 + + # Sorting angles according to x-y plane + order = np.argsort(np.array([_sort_angle(i / np.linalg.norm(i), [0, 1]) + for i in vectors[:, 0:2]])) + + ordered_vectors = vectors[order][:, [COLS.X, COLS.Y]] + + return [morphmath.angle_between_vectors(ordered_vectors[i], ordered_vectors[i - 1]) + for i, _ in enumerate(ordered_vectors)] + + +@feature(shape=(...,)) +def trunk_origin_radii(morph, neurite_type=NeuriteType.all): + """Radii of the trunk sections of neurites in a morph.""" + return [n.root_node.points[0][COLS.R] + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=(...,)) +def trunk_section_lengths(morph, neurite_type=NeuriteType.all): + """List of lengths of trunk sections of neurites in a morph.""" + return [morphmath.section_length(n.root_node.points) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=()) +def number_of_neurites(morph, neurite_type=NeuriteType.all): + """Number of neurites in a morph.""" + return sum(1 for _ in iter_neurites(morph, filt=is_type(neurite_type))) + + +@feature(shape=(...,)) +def neurite_volume_density(morph, neurite_type=NeuriteType.all): + """Get volume density per neurite.""" + return [nf.volume_density(n) + for n in iter_neurites(morph, filt=is_type(neurite_type))] + + +@feature(shape=(...,)) +def sholl_crossings(morph, center=None, radii=None, neurite_type=NeuriteType.all): + """Calculate crossings of neurites. + + Args: + morph(Morphology|list): morphology or a list of neurites + center(Point): center point, if None then soma center is taken + radii(iterable of floats): radii for which crossings will be counted, + if None then soma radius is taken + neurite_type(NeuriteType): Type of neurite to use. By default ``NeuriteType.all`` is used. + + Returns: + Array of same length as radii, with a count of the number of crossings + for the respective radius + + This function can also be used with a list of sections, as follow:: + + secs = (sec for sec in nm.iter_sections(morph) if complex_filter(sec)) + sholl = nm.features.neuritefunc.sholl_crossings(secs, + center=morph.soma.center, + radii=np.arange(0, 1000, 100)) + """ + def _count_crossings(neurite, radius): + """Used to count_crossings of segments in neurite with radius.""" + r2 = radius ** 2 + count = 0 + for start, end in iter_segments(neurite): + start_dist2, end_dist2 = (morphmath.point_dist2(center, start), + morphmath.point_dist2(center, end)) + + count += int(start_dist2 <= r2 <= end_dist2 or + end_dist2 <= r2 <= start_dist2) + + return count + + if center is None or radii is None: + assert isinstance(morph, Morphology) and morph.soma, \ + '`sholl_crossings` input error. If `center` or `radii` is not set then `morph` is ' \ + 'expected to be an instance of Morphology and have a soma.' + if center is None: + center = morph.soma.center + if radii is None: + radii = [morph.soma.radius] + return [sum(_count_crossings(neurite, r) + for neurite in iter_neurites(morph, filt=is_type(neurite_type))) + for r in radii] + + +@feature(shape=(...,)) +def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None): + """Perform Sholl frequency calculations on a morph. + + Args: + morph(Morphology): a morphology + neurite_type(NeuriteType): which neurites to operate on + step_size(float): step size between Sholl radii + bins(iterable of floats): custom binning to use for the Sholl radii. If None, it uses + intervals of step_size between min and max radii of ``morphologies``. + + Note: + Given a morphology, the soma center is used for the concentric circles, + which range from the soma radii, and the maximum radial distance + in steps of `step_size`. Each segment of the morphology is tested, so a neurite that + bends back on itself, and crosses the same Sholl radius will get counted as + having crossed multiple times. + """ + neurite_filter = is_type(neurite_type) + + if bins is None: + min_soma_edge = morph.soma.radius + max_radii = max(np.max(np.linalg.norm(n.points[:, COLS.XYZ], axis=1)) + for n in morph.neurites if neurite_filter(n)) + bins = np.arange(min_soma_edge, min_soma_edge + max_radii, step_size) + + return sholl_crossings(morph, morph.soma.center, bins, neurite_type) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py new file mode 100644 index 000000000..b6b6d565c --- /dev/null +++ b/neurom/features/neurite.py @@ -0,0 +1,504 @@ +# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Neurite features. + +Any public function from this namespace can be called via the features mechanism. If calling +directly the function in this namespace can only accept a neurite as its input. If you want to +apply it to anything other than neurite then you must use the features mechanism e.g. +``features.get``. + +>>> import neurom +>>> from neurom import features +>>> m = neurom.load_morphology('path/to/morphology') +>>> features.get('max_radial_distance', m.neurites[0]) +>>> features.get('max_radial_distance', m) +>>> features.get('number_of_segments', m.neurites, neurite_type=neurom.AXON) + +For more details see :ref:`features`. +""" + +import logging +from functools import partial +from itertools import chain + +import numpy as np +import scipy +from neurom import morphmath +from neurom.core.morphology import Section +from neurom.core.dataformat import COLS +from neurom.features import NameSpace, feature, bifurcation as bf, section as sf +from neurom.geom import convex_hull +from neurom.morphmath import interval_lengths + +feature = partial(feature, namespace=NameSpace.NEURITE) + +L = logging.getLogger(__name__) + + +def _map_sections(fun, neurite, iterator_type=Section.ipreorder): + """Map `fun` to all the sections.""" + return list(map(fun, (s for s in iterator_type(neurite.root_node)))) + + +@feature(shape=()) +def max_radial_distance(neurite): + """Get the maximum radial distances of the termination sections.""" + term_radial_distances = section_term_radial_distances(neurite) + return max(term_radial_distances) if term_radial_distances else 0. + + +@feature(shape=()) +def number_of_segments(neurite): + """Number of segments.""" + return sum(len(s.points) - 1 for s in Section.ipreorder(neurite.root_node)) + + +@feature(shape=()) +def number_of_sections(neurite, iterator_type=Section.ipreorder): + """Number of sections. For a morphology it will be a sum of all neurites sections numbers.""" + return sum(1 for _ in iterator_type(neurite.root_node)) + + +@feature(shape=()) +def number_of_bifurcations(neurite): + """Number of bf points.""" + return number_of_sections(neurite, iterator_type=Section.ibifurcation_point) + + +@feature(shape=()) +def number_of_forking_points(neurite): + """Number of forking points.""" + return number_of_sections(neurite, iterator_type=Section.iforking_point) + + +@feature(shape=()) +def number_of_leaves(neurite): + """Number of leaves points.""" + return number_of_sections(neurite, iterator_type=Section.ileaf) + + +@feature(shape=()) +def total_length(neurite): + """Neurite length. For a morphology it will be a sum of all neurite lengths.""" + return sum(s.length for s in neurite.iter_sections()) + + +@feature(shape=()) +def total_area(neurite): + """Neurite surface area. For a morphology it will be a sum of all neurite areas. + + The area is defined as the sum of the area of the sections. + """ + return neurite.area + + +@feature(shape=()) +def total_volume(neurite): + """Neurite volume. For a morphology it will be a sum of neurites volumes.""" + return sum(s.volume for s in Section.ipreorder(neurite.root_node)) + + +def _section_length(section): + """Get section length of `section`.""" + return morphmath.section_length(section.points) + + +@feature(shape=(...,)) +def section_lengths(neurite): + """Section lengths.""" + return _map_sections(_section_length, neurite) + + +@feature(shape=(...,)) +def section_term_lengths(neurite): + """Termination section lengths.""" + return _map_sections(_section_length, neurite, Section.ileaf) + + +@feature(shape=(...,)) +def section_bif_lengths(neurite): + """Bifurcation section lengths.""" + return _map_sections(_section_length, neurite, Section.ibifurcation_point) + + +@feature(shape=(...,)) +def section_branch_orders(neurite): + """Section branch orders.""" + return _map_sections(sf.branch_order, neurite) + + +@feature(shape=(...,)) +def section_bif_branch_orders(neurite): + """Bifurcation section branch orders.""" + return _map_sections(sf.branch_order, neurite, Section.ibifurcation_point) + + +@feature(shape=(...,)) +def section_term_branch_orders(neurite): + """Termination section branch orders.""" + return _map_sections(sf.branch_order, neurite, Section.ileaf) + + +@feature(shape=(...,)) +def section_path_distances(neurite): + """Path lengths.""" + + def pl2(node): + """Calculate the path length using cached section lengths.""" + return sum(n.length for n in node.iupstream()) + + return _map_sections(pl2, neurite) + + +################################################################################ +# Features returning one value per SEGMENT # +################################################################################ + + +def _map_segments(func, neurite): + """Map `func` to all the segments. + + `func` accepts a section and returns list of values corresponding to each segment. + """ + tmp = [mapped_seg for s in Section.ipreorder(neurite.root_node) for mapped_seg in func(s)] + return tmp + + +@feature(shape=(...,)) +def segment_lengths(neurite): + """Lengths of the segments.""" + return _map_segments(sf.segment_lengths, neurite) + + +@feature(shape=(...,)) +def segment_areas(neurite): + """Areas of the segments.""" + return [morphmath.segment_area(seg) + for s in Section.ipreorder(neurite.root_node) + for seg in zip(s.points[:-1], s.points[1:])] + + +@feature(shape=(...,)) +def segment_volumes(neurite): + """Volumes of the segments.""" + + def _func(sec): + """List of segment volumes of a section.""" + return [morphmath.segment_volume(seg) for seg in zip(sec.points[:-1], sec.points[1:])] + + return _map_segments(_func, neurite) + + +@feature(shape=(...,)) +def segment_radii(neurite): + """Arithmetic mean of the radii of the points in segments.""" + + def _seg_radii(sec): + """Vectorized mean radii.""" + pts = sec.points[:, COLS.R] + return np.divide(np.add(pts[:-1], pts[1:]), 2.0) + + return _map_segments(_seg_radii, neurite) + + +@feature(shape=(...,)) +def segment_taper_rates(neurite): + """Diameters taper rates of the segments. + + The taper rate is defined as the absolute radii differences divided by length of the section + """ + + def _seg_taper_rates(sec): + """Vectorized taper rates.""" + pts = sec.points[:, COLS.XYZR] + diff = np.diff(pts, axis=0) + distance = np.linalg.norm(diff[:, COLS.XYZ], axis=1) + return np.divide(2 * np.abs(diff[:, COLS.R]), distance) + + return _map_segments(_seg_taper_rates, neurite) + + +@feature(shape=(...,)) +def section_taper_rates(neurite): + """Diameter taper rates of the sections from root to tip. + + Taper rate is defined here as the linear fit along a section. + It is expected to be negative for morphologies. + """ + + def _sec_taper_rate(sec): + """Taper rate from fit along a section.""" + path_distances = np.cumsum(interval_lengths(sec.points, prepend_zero=True)) + return np.polynomial.polynomial.polyfit(path_distances, 2 * sec.points[:, COLS.R], 1)[1] + + return _map_sections(_sec_taper_rate, neurite) + + +@feature(shape=(...,)) +def segment_meander_angles(neurite): + """Inter-segment opening angles in a section.""" + return list(chain.from_iterable(_map_sections(sf.section_meander_angles, neurite))) + + +@feature(shape=(..., 3)) +def segment_midpoints(neurite): + """Return a list of segment mid-points.""" + + def _seg_midpoint(sec): + """Return the mid-points of segments in a section.""" + pts = sec.points[:, COLS.XYZ] + return np.divide(np.add(pts[:-1], pts[1:]), 2.0) + + return _map_segments(_seg_midpoint, neurite) + + +@feature(shape=(...,)) +def segment_path_lengths(neurite): + """Returns pathlengths between all non-root points and their root point.""" + pathlength = {} + + def _get_pathlength(section): + if section.id not in pathlength: + if section.parent: + pathlength[section.id] = section.parent.length + _get_pathlength(section.parent) + else: + pathlength[section.id] = 0 + return pathlength[section.id] + + result = [_get_pathlength(section) + np.cumsum(sf.segment_lengths(section)) + for section in Section.ipreorder(neurite.root_node)] + return np.hstack(result).tolist() if result else [] + + +@feature(shape=(...,)) +def segment_radial_distances(neurite, origin=None): + """Returns the list of distances between all segment mid points and origin.""" + + def _radial_distances(sec, pos): + """List of distances between the mid point of each segment and pos.""" + mid_pts = 0.5 * (sec.points[:-1, COLS.XYZ] + sec.points[1:, COLS.XYZ]) + return np.linalg.norm(mid_pts - pos[COLS.XYZ], axis=1) + + pos = neurite.root_node.points[0] if origin is None else origin + # return [s for ss in n.iter_sections() for s in _radial_distances(ss, pos)] + return [d for s in Section.ipreorder(neurite.root_node) for d in _radial_distances(s, pos)] + + +@feature(shape=(...,)) +def local_bifurcation_angles(neurite): + """Get a list of local bf angles.""" + return _map_sections(bf.local_bifurcation_angle, + neurite, + iterator_type=Section.ibifurcation_point) + + +@feature(shape=(...,)) +def remote_bifurcation_angles(neurite): + """Get a list of remote bf angles.""" + return _map_sections(bf.remote_bifurcation_angle, + neurite, + iterator_type=Section.ibifurcation_point) + + +@feature(shape=(...,)) +def partition_asymmetry(neurite, variant='branch-order', method='petilla'): + """Partition asymmetry at bf points. + + Variant: length is a different definition, as the absolute difference in + downstream path lenghts, relative to the total neurite path length + Method: 'petilla' or 'uylings'. The former is default. The latter uses ``-2`` shift. See + :func:`neurom.features.bifurcationfunc.partition_asymmetry` + """ + if variant not in {'branch-order', 'length'}: + raise ValueError('Please provide a valid variant for partition asymmetry,' + f'found {variant}') + if method not in {'petilla', 'uylings'}: + raise ValueError('Please provide a valid method for partition asymmetry,' + 'either "petilla" or "uylings"') + + if variant == 'branch-order': + return _map_sections( + partial(bf.partition_asymmetry, uylings=method == 'uylings'), + neurite, + Section.ibifurcation_point) + + asymmetries = [] + neurite_length = total_length(neurite) + for section in Section.ibifurcation_point(neurite.root_node): + pathlength_diff = abs(sf.downstream_pathlength(section.children[0]) - + sf.downstream_pathlength(section.children[1])) + asymmetries.append(pathlength_diff / neurite_length) + return asymmetries + + +@feature(shape=(...,)) +def partition_asymmetry_length(neurite, method='petilla'): + """'partition_asymmetry' feature with `variant='length'`. + + Because it is often used, it has a dedicated feature. + """ + return partition_asymmetry(neurite, 'length', method) + + +@feature(shape=(...,)) +def bifurcation_partitions(neurite): + """Partition at bf points.""" + return _map_sections(bf.bifurcation_partition, + neurite, + Section.ibifurcation_point) + + +@feature(shape=(...,)) +def sibling_ratios(neurite, method='first'): + """Sibling ratios at bf points. + + The sibling ratio is the ratio between the diameters of the + smallest and the largest child. It is a real number between + 0 and 1. Method argument allows one to consider mean diameters + along the child section instead of diameter of the first point. + """ + return _map_sections(partial(bf.sibling_ratio, method=method), + neurite, + Section.ibifurcation_point) + + +@feature(shape=(..., 2)) +def partition_pairs(neurite): + """Partition pairs at bf points. + + Partition pair is defined as the number of bifurcations at the two + daughters of the bifurcating section + """ + return _map_sections(bf.partition_pair, + neurite, + Section.ibifurcation_point) + + +@feature(shape=(...,)) +def diameter_power_relations(neurite, method='first'): + """Calculate the diameter power relation at a bf point. + + Diameter power relation is defined in https://www.ncbi.nlm.nih.gov/pubmed/18568015 + + This quantity gives an indication of how far the branching is from + the Rall ratio (when =1). + """ + return _map_sections(partial(bf.diameter_power_relation, method=method), + neurite, + Section.ibifurcation_point) + + +@feature(shape=(...,)) +def section_radial_distances(neurite, origin=None, iterator_type=Section.ipreorder): + """Section radial distances. + + The iterator_type can be used to select only terminal sections (ileaf) + or only bifurcations (ibifurcation_point). + """ + pos = neurite.root_node.points[0] if origin is None else origin + return _map_sections(partial(sf.section_radial_distance, origin=pos), + neurite, + iterator_type) + + +@feature(shape=(...,)) +def section_term_radial_distances(neurite, origin=None): + """Get the radial distances of the termination sections.""" + return section_radial_distances(neurite, origin, Section.ileaf) + + +@feature(shape=(...,)) +def section_bif_radial_distances(neurite, origin=None): + """Get the radial distances of the bf sections.""" + return section_radial_distances(neurite, origin, Section.ibifurcation_point) + + +@feature(shape=(...,)) +def terminal_path_lengths(neurite): + """Get the path lengths to each terminal point.""" + return _map_sections(sf.section_path_length, neurite, Section.ileaf) + + +@feature(shape=()) +def volume_density(neurite): + """Get the volume density. + + The volume density is defined as the ratio of the neurite volume and + the volume of the neurite's enclosing convex hull + + TODO: convex hull fails on some morphologies, it may be good to instead use + bounding_box to compute the neurite enclosing volume + + .. note:: Returns `np.nan` if the convex hull computation fails. + """ + try: + volume = convex_hull(neurite).volume + except scipy.spatial.qhull.QhullError: + L.exception('Failure to compute neurite volume using the convex hull. ' + 'Feature `volume_density` will return `np.nan`.\n') + return np.nan + + return neurite.volume / volume + + +@feature(shape=(...,)) +def section_volumes(neurite): + """Section volumes.""" + return _map_sections(sf.section_volume, neurite) + + +@feature(shape=(...,)) +def section_areas(neurite): + """Section areas.""" + return _map_sections(sf.section_area, neurite) + + +@feature(shape=(...,)) +def section_tortuosity(neurite): + """Section tortuosities.""" + return _map_sections(sf.section_tortuosity, neurite) + + +@feature(shape=(...,)) +def section_end_distances(neurite): + """Section end to end distances.""" + return _map_sections(sf.section_end_distance, neurite) + + +@feature(shape=(...,)) +def principal_direction_extents(neurite, direction=0): + """Principal direction extent of neurites in morphologies.""" + points = neurite.points[:, :3] + return [morphmath.principal_direction_extent(points)[direction]] + + +@feature(shape=(...,)) +def section_strahler_orders(neurite): + """Inter-segment opening angles in a section.""" + return _map_sections(sf.strahler_order, neurite) diff --git a/neurom/features/neuritefunc.py b/neurom/features/neuritefunc.py deleted file mode 100644 index c378f4dae..000000000 --- a/neurom/features/neuritefunc.py +++ /dev/null @@ -1,613 +0,0 @@ -# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Neurite features. - -Any public function from this namespace can be called via features mechanism on a neurite, a -collection of neurites, a neuron, a neuron population: - ->>> import neurom ->>> from neurom import features ->>> nrn = neurom.load_neuron('path/to/neuron') ->>> features.get('max_radial_distance', nrn.neurites) ->>> features.get('n_segments', nrn.neurites, neurite_type=neurom.AXON) -""" - -import logging -from functools import partial, update_wrapper -from itertools import chain - -import numpy as np -import scipy -from neurom import morphmath -from neurom.core.neuron import NeuriteType, Section, iter_neurites, iter_sections, iter_segments -from neurom.core.dataformat import COLS -from neurom.core.types import tree_type_checker as is_type -from neurom.features import _register_feature, bifurcationfunc, feature, sectionfunc -from neurom.features.neuronfunc import _neuron_population -from neurom.geom import convex_hull -from neurom.morphmath import interval_lengths - -feature = partial(feature, namespace='NEURITEFEATURES') - -L = logging.getLogger(__name__) - - -def _map_sections(fun, neurites, neurite_type=NeuriteType.all, iterator_type=Section.ipreorder): - """Map `fun` to all the sections in a collection of neurites.""" - return map(fun, iter_sections(neurites, - iterator_type=iterator_type, - neurite_filter=is_type(neurite_type))) - - -@feature(shape=()) -def max_radial_distance(neurites, neurite_type=NeuriteType.all): - """Get the maximum radial distances of the termination sections for a collection of neurites.""" - term_radial_distances = section_term_radial_distances(neurites, neurite_type) - return max(term_radial_distances) if term_radial_distances else 0. - - -@feature(shape=()) -def n_segments(neurites, neurite_type=NeuriteType.all): - """Number of segments in a collection of neurites.""" - return sum(len(s.points) - 1 - for s in iter_sections(neurites, neurite_filter=is_type(neurite_type))) - - -@feature(shape=()) -def n_neurites(neurites, neurite_type=NeuriteType.all): - """Number of neurites in a collection of neurites.""" - return sum(1 for _ in iter_neurites(neurites, filt=is_type(neurite_type))) - - -@feature(shape=()) -def n_sections(neurites, neurite_type=NeuriteType.all, iterator_type=Section.ipreorder): - """Number of sections in a collection of neurites.""" - return sum(1 for _ in iter_sections(neurites, - iterator_type=iterator_type, - neurite_filter=is_type(neurite_type))) - - -@feature(shape=()) -def n_bifurcation_points(neurites, neurite_type=NeuriteType.all): - """Number of bifurcation points in a collection of neurites.""" - return n_sections(neurites, neurite_type=neurite_type, iterator_type=Section.ibifurcation_point) - - -@feature(shape=()) -def n_forking_points(neurites, neurite_type=NeuriteType.all): - """Number of forking points in a collection of neurites.""" - return n_sections(neurites, neurite_type=neurite_type, iterator_type=Section.iforking_point) - - -@feature(shape=()) -def n_leaves(neurites, neurite_type=NeuriteType.all): - """Number of leaves points in a collection of neurites.""" - return n_sections(neurites, neurite_type=neurite_type, iterator_type=Section.ileaf) - - -@feature(shape=(...,)) -def total_area_per_neurite(neurites, neurite_type=NeuriteType.all): - """Surface area in a collection of neurites. - - The area is defined as the sum of the area of the sections. - """ - return [neurite.area for neurite in iter_neurites(neurites, filt=is_type(neurite_type))] - - -def _section_length(section): - """Get section length of `section`.""" - return morphmath.section_length(section.points) - - -@feature(shape=(...,)) -def section_lengths(neurites, neurite_type=NeuriteType.all): - """Section lengths in a collection of neurites.""" - return _map_sections(_section_length, neurites, neurite_type=neurite_type) - - -@feature(shape=(...,)) -def section_term_lengths(neurites, neurite_type=NeuriteType.all): - """Termination section lengths in a collection of neurites.""" - return _map_sections(_section_length, neurites, neurite_type=neurite_type, - iterator_type=Section.ileaf) - - -@feature(shape=(...,)) -def section_bif_lengths(neurites, neurite_type=NeuriteType.all): - """Bifurcation section lengths in a collection of neurites.""" - return _map_sections(_section_length, neurites, neurite_type=neurite_type, - iterator_type=Section.ibifurcation_point) - - -@feature(shape=(...,)) -def section_branch_orders(neurites, neurite_type=NeuriteType.all): - """Section branch orders in a collection of neurites.""" - return _map_sections(sectionfunc.branch_order, neurites, neurite_type=neurite_type) - - -@feature(shape=(...,)) -def section_bif_branch_orders(neurites, neurite_type=NeuriteType.all): - """Bifurcation section branch orders in a collection of neurites.""" - return _map_sections(sectionfunc.branch_order, neurites, neurite_type=neurite_type, - iterator_type=Section.ibifurcation_point) - - -@feature(shape=(...,)) -def section_term_branch_orders(neurites, neurite_type=NeuriteType.all): - """Termination section branch orders in a collection of neurites.""" - return _map_sections(sectionfunc.branch_order, neurites, neurite_type=neurite_type, - iterator_type=Section.ileaf) - - -@feature(shape=(...,), name='section_path_distances') -def section_path_lengths(neurites, neurite_type=NeuriteType.all): - """Path lengths of a collection of neurites.""" - # Calculates and stores the section lengths in one pass, - # then queries the lengths in the path length iterations. - # This avoids repeatedly calculating the lengths of the - # same sections. - def pl2(node): - """Calculate the path length using cached section lengths.""" - return sum(n.length for n in node.iupstream()) - - return _map_sections(pl2, neurites, neurite_type=neurite_type) - - -################################################################################ -# Features returning one value per NEURON # -################################################################################ - -def _map_neurons(fun, neurites, neurite_type): - """Map `fun` to all the neurites in a single or collection of neurons.""" - nrns = _neuron_population(neurites) - return [fun(n, neurite_type=neurite_type) for n in nrns] - - -@feature(shape=(...,)) -def max_radial_distances(neurites, neurite_type=NeuriteType.all): - """Get the maximum radial distances of the termination sections for a collection of neurites.""" - return _map_neurons(max_radial_distance, neurites, neurite_type) - - -@feature(shape=(...,)) -def number_of_sections(neurites, neurite_type=NeuriteType.all): - """Number of sections in a collection of neurites.""" - return _map_neurons(n_sections, neurites, neurite_type) - - -@feature(shape=(...,)) -def number_of_neurites(neurites, neurite_type=NeuriteType.all): - """Number of neurites in a collection of neurites.""" - return _map_neurons(n_neurites, neurites, neurite_type) - - -@feature(shape=(...,)) -def number_of_bifurcations(neurites, neurite_type=NeuriteType.all): - """Number of bifurcation points in a collection of neurites.""" - return _map_neurons(n_bifurcation_points, neurites, neurite_type) - - -@feature(shape=(...,)) -def number_of_forking_points(neurites, neurite_type=NeuriteType.all): - """Number of forking points in a collection of neurites.""" - return _map_neurons(n_forking_points, neurites, neurite_type) - - -@feature(shape=(...,)) -def number_of_terminations(neurites, neurite_type=NeuriteType.all): - """Number of leaves points in a collection of neurites.""" - return _map_neurons(n_leaves, neurites, neurite_type) - - -@feature(shape=(...,)) -def number_of_segments(neurites, neurite_type=NeuriteType.all): - """Number of sections in a collection of neurites.""" - return _map_neurons(n_segments, neurites, neurite_type) - -################################################################################ -# Features returning one value per SEGMENT # -################################################################################ - - -def map_segments(func, neurites, neurite_type): - """Map `func` to all the segments in a collection of neurites. - - `func` accepts a section and returns list of values corresponding to each segment. - """ - neurite_filter = is_type(neurite_type) - return [ - s for ss in iter_sections(neurites, neurite_filter=neurite_filter) for s in func(ss) - ] - - -@feature(shape=(...,)) -def segment_lengths(neurites, neurite_type=NeuriteType.all): - """Lengths of the segments in a collection of neurites.""" - return map_segments(sectionfunc.segment_lengths, neurites, neurite_type) - - -@feature(shape=(...,)) -def segment_areas(neurites, neurite_type=NeuriteType.all): - """Areas of the segments in a collection of neurites.""" - return [morphmath.segment_area(seg) for seg - in iter_segments(neurites, is_type(neurite_type))] - - -@feature(shape=(...,)) -def segment_volumes(neurites, neurite_type=NeuriteType.all): - """Volumes of the segments in a collection of neurites.""" - def _func(sec): - """List of segment volumes of a section.""" - return [morphmath.segment_volume(seg) for seg in zip(sec.points[:-1], sec.points[1:])] - - return map_segments(_func, neurites, neurite_type) - - -@feature(shape=(...,)) -def segment_radii(neurites, neurite_type=NeuriteType.all): - """Arithmetic mean of the radii of the points in segments in a collection of neurites.""" - def _seg_radii(sec): - """Vectorized mean radii.""" - pts = sec.points[:, COLS.R] - return np.divide(np.add(pts[:-1], pts[1:]), 2.0) - - return map_segments(_seg_radii, neurites, neurite_type) - - -@feature(shape=(...,)) -def segment_taper_rates(neurites, neurite_type=NeuriteType.all): - """Diameters taper rates of the segments in a collection of neurites. - - The taper rate is defined as the absolute radii differences divided by length of the section - """ - def _seg_taper_rates(sec): - """Vectorized taper rates.""" - pts = sec.points[:, COLS.XYZR] - diff = np.diff(pts, axis=0) - distance = np.linalg.norm(diff[:, COLS.XYZ], axis=1) - return np.divide(2 * np.abs(diff[:, COLS.R]), distance) - - return map_segments(_seg_taper_rates, neurites, neurite_type) - - -@feature(shape=(...,)) -def section_taper_rates(neurites, neurite_type=NeuriteType.all): - """Diameter taper rates of the sections in a collection of neurites from root to tip. - - Taper rate is defined here as the linear fit along a section. - It is expected to be negative for neurons. - """ - def _sec_taper_rate(sec): - """Taper rate from fit along a section.""" - path_distances = np.cumsum(interval_lengths(sec.points, prepend_zero=True)) - return np.polynomial.polynomial.polyfit(path_distances, 2 * sec.points[:, COLS.R], 1)[1] - - return _map_sections(_sec_taper_rate, neurites, neurite_type=neurite_type) - - -@feature(shape=(...,)) -def segment_meander_angles(neurites, neurite_type=NeuriteType.all): - """Inter-segment opening angles in a section.""" - return list(chain.from_iterable(_map_sections( - sectionfunc.section_meander_angles, neurites, neurite_type))) - - -@feature(shape=(..., 3)) -def segment_midpoints(neurites, neurite_type=NeuriteType.all): - """Return a list of segment mid-points in a collection of neurites.""" - def _seg_midpoint(sec): - """Return the mid-points of segments in a section.""" - pts = sec.points[:, COLS.XYZ] - return np.divide(np.add(pts[:-1], pts[1:]), 2.0) - - return map_segments(_seg_midpoint, neurites, neurite_type) - - -@feature(shape=(...,)) -def segment_path_lengths(neurites, neurite_type=NeuriteType.all): - """Returns pathlengths between all non-root points and their root point.""" - pathlength = {} - neurite_filter = is_type(neurite_type) - - def _get_pathlength(section): - if section.id not in pathlength: - if section.parent: - pathlength[section.id] = section.parent.length + _get_pathlength(section.parent) - else: - pathlength[section.id] = 0 - return pathlength[section.id] - - result = [_get_pathlength(section) + np.cumsum(sectionfunc.segment_lengths(section)) - for section in iter_sections(neurites, neurite_filter=neurite_filter)] - return np.hstack(result) if result else np.array([]) - - -@feature(shape=(...,)) -def segment_radial_distances(neurites, neurite_type=NeuriteType.all, origin=None): - """Returns the list of distances between all segment mid points and origin.""" - def _radial_distances(sec, pos): - """List of distances between the mid point of each segment and pos.""" - mid_pts = 0.5 * (sec.points[:-1, COLS.XYZ] + sec.points[1:, COLS.XYZ]) - return np.linalg.norm(mid_pts - pos[COLS.XYZ], axis=1) - - dist = [] - for n in iter_neurites(neurites, filt=is_type(neurite_type)): - pos = n.root_node.points[0] if origin is None else origin - dist.extend([s for ss in n.iter_sections() for s in _radial_distances(ss, pos)]) - - return dist - - -@feature(shape=(...,)) -def local_bifurcation_angles(neurites, neurite_type=NeuriteType.all): - """Get a list of local bifurcation angles in a collection of neurites.""" - return _map_sections(bifurcationfunc.local_bifurcation_angle, - neurites, - neurite_type=neurite_type, - iterator_type=Section.ibifurcation_point) - - -@feature(shape=(...,)) -def remote_bifurcation_angles(neurites, neurite_type=NeuriteType.all): - """Get a list of remote bifurcation angles in a collection of neurites.""" - return _map_sections(bifurcationfunc.remote_bifurcation_angle, - neurites, - neurite_type=neurite_type, - iterator_type=Section.ibifurcation_point) - - -@feature(shape=(...,), name='partition_asymmetry') -def partition_asymmetries(neurites, - neurite_type=NeuriteType.all, - variant='branch-order', - method='petilla'): - """Partition asymmetry at bifurcation points of a collection of neurites. - - Variant: length is a different definition, as the absolute difference in - downstream path lenghts, relative to the total neurite path length - Method: 'petilla' or 'uylings'. The former is default. The latter uses ``-2`` shift. See - :func:`neurom.features.bifurcationfunc.partition_asymmetry` - """ - if variant not in {'branch-order', 'length'}: - raise ValueError('Please provide a valid variant for partition asymmetry,' - f'found {variant}') - if method not in {'petilla', 'uylings'}: - raise ValueError('Please provide a valid method for partition asymmetry,' - 'either "petilla" or "uylings"') - - if variant == 'branch-order': - return map(partial(bifurcationfunc.partition_asymmetry, uylings=method == 'uylings'), - iter_sections(neurites, - iterator_type=Section.ibifurcation_point, - neurite_filter=is_type(neurite_type))) - - asymmetries = list() - for neurite in iter_neurites(neurites, filt=is_type(neurite_type)): - neurite_length = total_length_per_neurite(neurite)[0] - for section in iter_sections(neurite, - iterator_type=Section.ibifurcation_point, - neurite_filter=is_type(neurite_type)): - pathlength_diff = abs(sectionfunc.downstream_pathlength(section.children[0]) - - sectionfunc.downstream_pathlength(section.children[1])) - asymmetries.append(pathlength_diff / neurite_length) - return asymmetries - - -@feature(shape=(...,), name='partition') -def bifurcation_partitions(neurites, neurite_type=NeuriteType.all): - """Partition at bifurcation points of a collection of neurites.""" - return map(bifurcationfunc.bifurcation_partition, - iter_sections(neurites, - iterator_type=Section.ibifurcation_point, - neurite_filter=is_type(neurite_type))) - - -# Register `partition_asymmetries` variant -_partition_asymmetry_length = partial(partition_asymmetries, variant='length') -update_wrapper(_partition_asymmetry_length, partition_asymmetries) # this fixes the docstring -_register_feature('NEURITEFEATURES', 'partition_asymmetry_length', - _partition_asymmetry_length, shape=(...,)) - - -@feature(shape=(...,)) -def sibling_ratios(neurites, neurite_type=NeuriteType.all, method='first'): - """Sibling ratios at bifurcation points of a collection of neurites. - - The sibling ratio is the ratio between the diameters of the - smallest and the largest child. It is a real number between - 0 and 1. Method argument allows one to consider mean diameters - along the child section instead of diameter of the first point. - """ - return map(lambda bif_point: bifurcationfunc.sibling_ratio(bif_point, method), - iter_sections(neurites, - iterator_type=Section.ibifurcation_point, - neurite_filter=is_type(neurite_type))) - - -@feature(shape=(..., 2)) -def partition_pairs(neurites, neurite_type=NeuriteType.all): - """Partition pairs at bifurcation points of a collection of neurites. - - Partition pair is defined as the number of bifurcations at the two - daughters of the bifurcating section - """ - return map(bifurcationfunc.partition_pair, - iter_sections(neurites, - iterator_type=Section.ibifurcation_point, - neurite_filter=is_type(neurite_type))) - - -@feature(shape=(...,)) -def diameter_power_relations(neurites, neurite_type=NeuriteType.all, method='first'): - """Calculate the diameter power relation at a bifurcation point. - - Diameter power relation is defined in https://www.ncbi.nlm.nih.gov/pubmed/18568015 - - This quantity gives an indication of how far the branching is from - the Rall ratio (when =1). - """ - return (bifurcationfunc.diameter_power_relation(bif_point, method) - for bif_point in iter_sections(neurites, - iterator_type=Section.ibifurcation_point, - neurite_filter=is_type(neurite_type))) - - -@feature(shape=(...,)) -def section_radial_distances(neurites, neurite_type=NeuriteType.all, origin=None, - iterator_type=Section.ipreorder): - """Section radial distances in a collection of neurites. - - The iterator_type can be used to select only terminal sections (ileaf) - or only bifurcations (ibifurcation_point). - """ - dist = [] - for n in iter_neurites(neurites, filt=is_type(neurite_type)): - pos = n.root_node.points[0] if origin is None else origin - dist.extend(sectionfunc.section_radial_distance(s, pos) - for s in iter_sections(n, - iterator_type=iterator_type)) - return dist - - -@feature(shape=(...,)) -def section_term_radial_distances(neurites, neurite_type=NeuriteType.all, origin=None): - """Get the radial distances of the termination sections for a collection of neurites.""" - return section_radial_distances(neurites, neurite_type=neurite_type, origin=origin, - iterator_type=Section.ileaf) - - -@feature(shape=(...,)) -def section_bif_radial_distances(neurites, neurite_type=NeuriteType.all, origin=None): - """Get the radial distances of the bifurcation sections for a collection of neurites.""" - return section_radial_distances(neurites, neurite_type=neurite_type, origin=origin, - iterator_type=Section.ibifurcation_point) - - -@feature(shape=(...,)) -def number_of_sections_per_neurite(neurites, neurite_type=NeuriteType.all): - """Get the number of sections per neurite in a collection of neurites.""" - return list(sum(1 for _ in n.iter_sections()) - for n in iter_neurites(neurites, filt=is_type(neurite_type))) - - -@feature(shape=(...,)) -def total_length_per_neurite(neurites, neurite_type=NeuriteType.all): - """Get the path length per neurite in a collection.""" - return list(sum(s.length for s in n.iter_sections()) - for n in iter_neurites(neurites, filt=is_type(neurite_type))) - - -@feature(shape=(...,)) -def neurite_lengths(neurites, neurite_type=NeuriteType.all): - """Get the path length per neurite in a collection.""" - return total_length_per_neurite(neurites, neurite_type) - - -@feature(shape=(...,)) -def terminal_path_lengths_per_neurite(neurites, neurite_type=NeuriteType.all): - """Get the path lengths to each terminal point per neurite in a collection.""" - return list(sectionfunc.section_path_length(s) - for n in iter_neurites(neurites, filt=is_type(neurite_type)) - for s in iter_sections(n, iterator_type=Section.ileaf)) - - -@feature(shape=(...,), name='neurite_volumes') -def total_volume_per_neurite(neurites, neurite_type=NeuriteType.all): - """Get the volume per neurite in a collection.""" - return list(sum(s.volume for s in n.iter_sections()) - for n in iter_neurites(neurites, filt=is_type(neurite_type))) - - -@feature(shape=(...,)) -def neurite_volume_density(neurites, neurite_type=NeuriteType.all): - """Get the volume density per neurite. - - The volume density is defined as the ratio of the neurite volume and - the volume of the neurite's enclosing convex hull - - TODO: the convex hull fails on some morphologies, it may be good to instead use - bounding_box to compute the neurite enclosing volume - - .. note:: Returns `np.nan` if the convex hull computation fails. - """ - def vol_density(neurite): - """Volume density of a single neurite.""" - try: - volume = convex_hull(neurite).volume - except scipy.spatial.qhull.QhullError: - L.exception('Failure to compute neurite volume using the convex hull. ' - 'Feature `neurite_volume_density` will return `np.nan`.\n') - return np.nan - - return neurite.volume / volume - - return list(vol_density(n) - for n in iter_neurites(neurites, filt=is_type(neurite_type))) - - -@feature(shape=(...,)) -def section_volumes(neurites, neurite_type=NeuriteType.all): - """Section volumes in a collection of neurites.""" - return _map_sections(sectionfunc.section_volume, neurites, neurite_type=neurite_type) - - -@feature(shape=(...,)) -def section_areas(neurites, neurite_type=NeuriteType.all): - """Section areas in a collection of neurites.""" - return _map_sections(sectionfunc.section_area, neurites, neurite_type=neurite_type) - - -@feature(shape=(...,)) -def section_tortuosity(neurites, neurite_type=NeuriteType.all): - """Section tortuosities in a collection of neurites.""" - return _map_sections(sectionfunc.section_tortuosity, neurites, neurite_type=neurite_type) - - -@feature(shape=(...,)) -def section_end_distances(neurites, neurite_type=NeuriteType.all): - """Section end to end distances in a collection of neurites.""" - return _map_sections(sectionfunc.section_end_distance, neurites, neurite_type=neurite_type) - - -@feature(shape=(...,)) -def principal_direction_extents(neurites, neurite_type=NeuriteType.all, direction=0): - """Principal direction extent of neurites in neurons.""" - def _pde(neurite): - """Get the PDE of a single neurite.""" - # Get the X, Y,Z coordinates of the points in each section - points = neurite.points[:, :3] - return morphmath.principal_direction_extent(points)[direction] - - return [_pde(neurite) for neurite in iter_neurites(neurites, filt=is_type(neurite_type))] - - -@feature(shape=(...,)) -def section_strahler_orders(neurites, neurite_type=NeuriteType.all): - """Inter-segment opening angles in a section.""" - return _map_sections(sectionfunc.strahler_order, neurites, neurite_type) diff --git a/neurom/features/neuronfunc.py b/neurom/features/neuronfunc.py deleted file mode 100644 index 5c9bdf3cc..000000000 --- a/neurom/features/neuronfunc.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Neuron features. - -Any public function from this namespace can be called via features mechanism on a neuron, a -neuron population: - ->>> import neurom ->>> from neurom import features ->>> nrn = neurom.load_neuron('path/to/neuron') ->>> features.get('soma_surface_area', nrn) ->>> nrn_population = neurom.load_neurons('path/to/neurons') ->>> features.get('sholl_frequency', nrn_population) -""" - - -from functools import partial -import math - -import numpy as np - -from neurom import morphmath -from neurom.core import Population -from neurom.core.neuron import iter_neurites, iter_segments -from neurom.core.dataformat import COLS -from neurom.core.types import NeuriteType -from neurom.core.types import tree_type_checker as is_type -from neurom.features import feature, neuritefunc - -feature = partial(feature, namespace='NEURONFEATURES') - - -def _neuron_population(nrns): - """Makes sure `nrns` behaves like a neuron population.""" - return nrns if isinstance(nrns, Population) else (nrns,) - - -@feature(shape=()) -def soma_volume(nrn): - """Get the volume of a neuron's soma.""" - return nrn.soma.volume - - -@feature(shape=(...,)) -def soma_volumes(nrn_pop): - """Get the volume of the somata in a population of neurons. - - Note: - If a single neuron is passed, a single element list with the volume - of its soma member is returned. - """ - nrns = _neuron_population(nrn_pop) - return [soma_volume(n) for n in nrns] - - -@feature(shape=()) -def soma_surface_area(nrn, neurite_type=NeuriteType.soma): - """Get the surface area of a neuron's soma. - - Note: - The surface area is calculated by assuming the soma is spherical. - """ - assert neurite_type == NeuriteType.soma, 'Neurite type must be soma' - return 4 * math.pi * nrn.soma.radius ** 2 - - -@feature(shape=(...,)) -def soma_surface_areas(nrn_pop, neurite_type=NeuriteType.soma): - """Get the surface areas of the somata in a population of neurons. - - Note: - The surface area is calculated by assuming the soma is spherical. - - Note: - If a single neuron is passed, a single element list with the surface - area of its soma member is returned. - """ - nrns = _neuron_population(nrn_pop) - assert neurite_type == NeuriteType.soma, 'Neurite type must be soma' - return [soma_surface_area(n) for n in nrns] - - -@feature(shape=(...,)) -def soma_radii(nrn_pop, neurite_type=NeuriteType.soma): - """Get the radii of the somata of a population of neurons. - - Note: - If a single neuron is passed, a single element list with the - radius of its soma member is returned. - """ - assert neurite_type == NeuriteType.soma, 'Neurite type must be soma' - nrns = _neuron_population(nrn_pop) - return [n.soma.radius for n in nrns] - - -@feature(shape=(...,)) -def trunk_section_lengths(nrn, neurite_type=NeuriteType.all): - """List of lengths of trunk sections of neurites in a neuron.""" - neurite_filter = is_type(neurite_type) - return [morphmath.section_length(s.root_node.points) - for s in nrn.neurites if neurite_filter(s)] - - -@feature(shape=(...,)) -def trunk_origin_radii(nrn, neurite_type=NeuriteType.all): - """Radii of the trunk sections of neurites in a neuron.""" - neurite_filter = is_type(neurite_type) - return [s.root_node.points[0][COLS.R] for s in nrn.neurites if neurite_filter(s)] - - -@feature(shape=(...,)) -def trunk_origin_azimuths(nrn, neurite_type=NeuriteType.all): - """Get a list of all the trunk origin azimuths of a neuron or population. - - The azimuth is defined as Angle between x-axis and the vector - defined by (initial tree point - soma center) on the x-z plane. - - The range of the azimuth angle [-pi, pi] radians - """ - neurite_filter = is_type(neurite_type) - nrns = _neuron_population(nrn) - - def _azimuth(section, soma): - """Azimuth of a section.""" - vector = morphmath.vector(section[0], soma.center) - return np.arctan2(vector[COLS.Z], vector[COLS.X]) - - return [_azimuth(s.root_node.points, n.soma) - for n in nrns - for s in n.neurites if neurite_filter(s)] - - -@feature(shape=(...,)) -def trunk_origin_elevations(nrn, neurite_type=NeuriteType.all): - """Get a list of all the trunk origin elevations of a neuron or population. - - The elevation is defined as the angle between x-axis and the - vector defined by (initial tree point - soma center) - on the x-y half-plane. - - The range of the elevation angle [-pi/2, pi/2] radians - """ - neurite_filter = is_type(neurite_type) - nrns = _neuron_population(nrn) - - def _elevation(section, soma): - """Elevation of a section.""" - vector = morphmath.vector(section[0], soma.center) - norm_vector = np.linalg.norm(vector) - - if norm_vector >= np.finfo(type(norm_vector)).eps: - return np.arcsin(vector[COLS.Y] / norm_vector) - raise ValueError("Norm of vector between soma center and section is almost zero.") - - return [_elevation(s.root_node.points, n.soma) - for n in nrns - for s in n.neurites if neurite_filter(s)] - - -@feature(shape=(...,)) -def trunk_vectors(nrn, neurite_type=NeuriteType.all): - """Calculates the vectors between all the trunks of the neuron and the soma center.""" - neurite_filter = is_type(neurite_type) - nrns = _neuron_population(nrn) - - return np.array([morphmath.vector(s.root_node.points[0], n.soma.center) - for n in nrns - for s in n.neurites if neurite_filter(s)]) - - -@feature(shape=(...,)) -def trunk_angles(nrn, neurite_type=NeuriteType.all): - """Calculates the angles between all the trunks of the neuron. - - The angles are defined on the x-y plane and the trees - are sorted from the y axis and anticlock-wise. - """ - vectors = trunk_vectors(nrn, neurite_type=neurite_type) - # In order to avoid the failure of the process in case the neurite_type does not exist - if not vectors.size: - return [] - - def _sort_angle(p1, p2): - """Angle between p1-p2 to sort vectors.""" - ang1 = np.arctan2(*p1[::-1]) - ang2 = np.arctan2(*p2[::-1]) - return ang1 - ang2 - - # Sorting angles according to x-y plane - order = np.argsort(np.array([_sort_angle(i / np.linalg.norm(i), [0, 1]) - for i in vectors[:, 0:2]])) - - ordered_vectors = vectors[order][:, [COLS.X, COLS.Y]] - - return [morphmath.angle_between_vectors(ordered_vectors[i], ordered_vectors[i - 1]) - for i, _ in enumerate(ordered_vectors)] - - -def sholl_crossings(neurites, center, radii, neurite_type=NeuriteType.all): - """Calculate crossings of neurites. The only function in this module that is not a feature. - - Args: - neurites(list): morphology on which to perform Sholl analysis, or list of neurites - center(Point): center point - radii(iterable of floats): radii for which crossings will be counted - neurite_type(NeuriteType): Type of neurite to use. By default ``NeuriteType.all`` is used. - - Returns: - Array of same length as radii, with a count of the number of crossings - for the respective radius - - This function can also be used with a list of sections, as follow:: - - secs = (sec for sec in nm.iter_sections(neuron) if complex_filter(sec)) - sholl = nm.features.neuritefunc.sholl_crossings(secs, - center=neuron.soma.center, - radii=np.arange(0, 1000, 100)) - """ - def _count_crossings(neurite, radius): - """Used to count_crossings of segments in neurite with radius.""" - r2 = radius ** 2 - count = 0 - for start, end in iter_segments(neurite): - start_dist2, end_dist2 = (morphmath.point_dist2(center, start), - morphmath.point_dist2(center, end)) - - count += int(start_dist2 <= r2 <= end_dist2 or - end_dist2 <= r2 <= start_dist2) - - return count - - return np.array([sum(_count_crossings(neurite, r) - for neurite in iter_neurites(neurites, filt=is_type(neurite_type))) - for r in radii]) - - -@feature(shape=(...,)) -def sholl_frequency(nrn, neurite_type=NeuriteType.all, step_size=10, bins=None): - """Perform Sholl frequency calculations on a population of neurites. - - Args: - nrn(morph): nrn or population - neurite_type(NeuriteType): which neurites to operate on - step_size(float): step size between Sholl radii - bins(iterable of floats): custom binning to use for the Sholl radii. If None, it uses - intervals of step_size between min and max radii of ``nrn``. - - Note: - Given a neuron, the soma center is used for the concentric circles, - which range from the soma radii, and the maximum radial distance - in steps of `step_size`. When a population is given, the concentric - circles range from the smallest soma radius to the largest radial neurite - distance. Finally, each segment of the neuron is tested, so a neurite that - bends back on itself, and crosses the same Sholl radius will get counted as - having crossed multiple times. - """ - nrns = _neuron_population(nrn) - - if bins is None: - min_soma_edge = min(neuron.soma.radius for neuron in nrns) - max_radii = max(np.max(np.linalg.norm(neurite.points[:, COLS.XYZ], axis=1)) - for nrn in nrns - for neurite in iter_neurites(nrn, filt=is_type(neurite_type))) - bins = np.arange(min_soma_edge, min_soma_edge + max_radii, step_size) - - return sum(sholl_crossings(neuron, neuron.soma.center, bins, neurite_type) - for neuron in nrns) - - -@feature(shape=(...,)) -def total_length(nrn_pop, neurite_type=NeuriteType.all): - """Get the total length of all sections in the group of neurons or neurites.""" - nrns = _neuron_population(nrn_pop) - return list(sum(neuritefunc.section_lengths(n, neurite_type=neurite_type)) for n in nrns) diff --git a/neurom/features/population.py b/neurom/features/population.py new file mode 100644 index 000000000..5e88f1499 --- /dev/null +++ b/neurom/features/population.py @@ -0,0 +1,82 @@ +# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Population features. + +Any public function from this namespace can be called via features mechanism. Functions in this +namespace can only accept a morphology population as its input no matter how called. + +>>> import neurom +>>> from neurom import features +>>> pop = neurom.load_morphologies('path/to/morphs') +>>> features.get('sholl_frequency', pop) + +For more details see :ref:`features`. +""" + + +from functools import partial +import numpy as np + +from neurom.core.dataformat import COLS +from neurom.core.types import NeuriteType +from neurom.core.types import tree_type_checker as is_type +from neurom.features import feature, NameSpace +from neurom.features.morphology import sholl_crossings + +feature = partial(feature, namespace=NameSpace.POPULATION) + + +@feature(shape=(...,)) +def sholl_frequency(morphs, neurite_type=NeuriteType.all, step_size=10, bins=None): + """Perform Sholl frequency calculations on a population of morphs. + + Args: + morphs(list|Population): list of morphologies or morphology population + neurite_type(NeuriteType): which neurites to operate on + step_size(float): step size between Sholl radii + bins(iterable of floats): custom binning to use for the Sholl radii. If None, it uses + intervals of step_size between min and max radii of ``morphs``. + + Note: + Given a population, the concentric circles range from the smallest soma radius to the + largest radial neurite distance in steps of `step_size`. Each segment of the morphology is + tested, so a neurite that bends back on itself, and crosses the same Sholl radius will + get counted as having crossed multiple times. + """ + neurite_filter = is_type(neurite_type) + + if bins is None: + min_soma_edge = min(n.soma.radius for n in morphs) + max_radii = max(np.max(np.linalg.norm(n.points[:, COLS.XYZ], axis=1)) + for m in morphs + for n in m.neurites if neurite_filter(n)) + bins = np.arange(min_soma_edge, min_soma_edge + max_radii, step_size) + + return sum(np.array(sholl_crossings(m, m.soma.center, bins, neurite_type)) + for m in morphs) diff --git a/neurom/features/section.py b/neurom/features/section.py new file mode 100644 index 000000000..cb8da3799 --- /dev/null +++ b/neurom/features/section.py @@ -0,0 +1,165 @@ +# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Section functions and functional tools.""" + +import numpy as np + +from neurom import morphmath as mm +from neurom.core.dataformat import COLS +from neurom.morphmath import interval_lengths + + +def section_path_length(section): + """Path length from section to root.""" + return sum(s.length for s in section.iupstream()) + + +def section_volume(section): + """Volume of a section.""" + return section.volume + + +def section_area(section): + """Surface area of a section.""" + return section.area + + +def section_tortuosity(section): + """Tortuosity of a section. + + The tortuosity is defined as the ratio of the path length of a section + and the euclidian distnce between its end points. + + The path length is the sum of distances between consecutive points. + + If the section contains less than 2 points, the value 1 is returned. + """ + pts = section.points + return 1 if len(pts) < 2 else mm.section_length(pts) / mm.point_dist(pts[-1], pts[0]) + + +def section_end_distance(section): + """End to end distance of a section. + + The end to end distance of a section is defined as + the euclidian distnce between its end points. + + If the section contains less than 2 points, the value 0 is returned. + """ + pts = section.points + return 0 if len(pts) < 2 else mm.point_dist(pts[-1], pts[0]) + + +def branch_order(section): + """Branching order of a tree section. + + The branching order is defined as the depth of the tree section. + + Note: + The first level has branch order 1. + """ + return sum(1 for _ in section.iupstream()) - 1 + + +def segment_lengths(section, prepend_zero=False): + """Returns the list of segment lengths within the section.""" + return interval_lengths(section.points, prepend_zero=prepend_zero) + + +def section_radial_distance(section, origin): + """Return the radial distances of a tree section to a given origin point. + + The radial distance is the euclidian distance between the + end-point point of the section and the origin point in question. + + Arguments: + section: neurite section object + origin: point to which distances are measured. It must have at least 3\ + components. The first 3 components are (x, y, z). + """ + return mm.point_dist(section.points[-1], origin) + + +def section_meander_angles(section): + """Inter-segment opening angles in a section.""" + p = section.points + return [mm.angle_3points(p[i - 1], p[i - 2], p[i]) + for i in range(2, len(p))] + + +def strahler_order(section): + """Branching order of a tree section. + + The strahler order is the inverse of the branch order, + since this is computed from the tips of the tree + towards the root. + + This implementation is a translation of the three steps described in + Wikipedia (https://en.wikipedia.org/wiki/Strahler_number): + + - If the node is a leaf (has no children), its Strahler number is one. + - If the node has one child with Strahler number i, and all other children + have Strahler numbers less than i, then the Strahler number of the node + is i again. + - If the node has two or more children with Strahler number i, and no + children with greater number, then the Strahler number of the node is + i + 1. + + No efforts have been invested in making it computationnaly efficient, but + it computes acceptably fast on tested morphologies (i.e., no waiting time). + """ + if section.children: + child_orders = [strahler_order(child) for child in section.children] + max_so_children = max(child_orders) + it = iter(co == max_so_children for co in child_orders) + # check if there are *two* or more children w/ the max_so_children + any(it) + if any(it): + return max_so_children + 1 + return max_so_children + return 1 + + +def locate_segment_position(section, fraction): + """Segment ID / offset corresponding to a given fraction of section length.""" + return mm.path_fraction_id_offset(section.points, fraction) + + +def section_mean_radius(section): + """Compute the mean radius of a section weighted by segment lengths.""" + radii = section.points[:, COLS.R] + points = section.points[:, COLS.XYZ] + lengths = np.linalg.norm(points[1:] - points[:-1], axis=1) + mean_radii = 0.5 * (radii[1:] + radii[:-1]) + return np.sum(mean_radii * lengths) / np.sum(lengths) + + +def downstream_pathlength(section): + """Compute the total downstream length starting from a section.""" + return sum(sec.length for sec in section.ipreorder()) diff --git a/neurom/features/sectionfunc.py b/neurom/features/sectionfunc.py index cb8da3799..82549fcaa 100644 --- a/neurom/features/sectionfunc.py +++ b/neurom/features/sectionfunc.py @@ -1,165 +1,8 @@ -# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""For backward compatibility only.""" +# pylint: skip-file -"""Section functions and functional tools.""" +from neurom.features.section import * # pragma: no cover +from neurom.utils import deprecated_module # pragma: no cover -import numpy as np - -from neurom import morphmath as mm -from neurom.core.dataformat import COLS -from neurom.morphmath import interval_lengths - - -def section_path_length(section): - """Path length from section to root.""" - return sum(s.length for s in section.iupstream()) - - -def section_volume(section): - """Volume of a section.""" - return section.volume - - -def section_area(section): - """Surface area of a section.""" - return section.area - - -def section_tortuosity(section): - """Tortuosity of a section. - - The tortuosity is defined as the ratio of the path length of a section - and the euclidian distnce between its end points. - - The path length is the sum of distances between consecutive points. - - If the section contains less than 2 points, the value 1 is returned. - """ - pts = section.points - return 1 if len(pts) < 2 else mm.section_length(pts) / mm.point_dist(pts[-1], pts[0]) - - -def section_end_distance(section): - """End to end distance of a section. - - The end to end distance of a section is defined as - the euclidian distnce between its end points. - - If the section contains less than 2 points, the value 0 is returned. - """ - pts = section.points - return 0 if len(pts) < 2 else mm.point_dist(pts[-1], pts[0]) - - -def branch_order(section): - """Branching order of a tree section. - - The branching order is defined as the depth of the tree section. - - Note: - The first level has branch order 1. - """ - return sum(1 for _ in section.iupstream()) - 1 - - -def segment_lengths(section, prepend_zero=False): - """Returns the list of segment lengths within the section.""" - return interval_lengths(section.points, prepend_zero=prepend_zero) - - -def section_radial_distance(section, origin): - """Return the radial distances of a tree section to a given origin point. - - The radial distance is the euclidian distance between the - end-point point of the section and the origin point in question. - - Arguments: - section: neurite section object - origin: point to which distances are measured. It must have at least 3\ - components. The first 3 components are (x, y, z). - """ - return mm.point_dist(section.points[-1], origin) - - -def section_meander_angles(section): - """Inter-segment opening angles in a section.""" - p = section.points - return [mm.angle_3points(p[i - 1], p[i - 2], p[i]) - for i in range(2, len(p))] - - -def strahler_order(section): - """Branching order of a tree section. - - The strahler order is the inverse of the branch order, - since this is computed from the tips of the tree - towards the root. - - This implementation is a translation of the three steps described in - Wikipedia (https://en.wikipedia.org/wiki/Strahler_number): - - - If the node is a leaf (has no children), its Strahler number is one. - - If the node has one child with Strahler number i, and all other children - have Strahler numbers less than i, then the Strahler number of the node - is i again. - - If the node has two or more children with Strahler number i, and no - children with greater number, then the Strahler number of the node is - i + 1. - - No efforts have been invested in making it computationnaly efficient, but - it computes acceptably fast on tested morphologies (i.e., no waiting time). - """ - if section.children: - child_orders = [strahler_order(child) for child in section.children] - max_so_children = max(child_orders) - it = iter(co == max_so_children for co in child_orders) - # check if there are *two* or more children w/ the max_so_children - any(it) - if any(it): - return max_so_children + 1 - return max_so_children - return 1 - - -def locate_segment_position(section, fraction): - """Segment ID / offset corresponding to a given fraction of section length.""" - return mm.path_fraction_id_offset(section.points, fraction) - - -def section_mean_radius(section): - """Compute the mean radius of a section weighted by segment lengths.""" - radii = section.points[:, COLS.R] - points = section.points[:, COLS.XYZ] - lengths = np.linalg.norm(points[1:] - points[:-1], axis=1) - mean_radii = 0.5 * (radii[1:] + radii[:-1]) - return np.sum(mean_radii * lengths) / np.sum(lengths) - - -def downstream_pathlength(section): - """Compute the total downstream length starting from a section.""" - return sum(sec.length for sec in section.ipreorder()) +deprecated_module('Module `neurom.features.sectionfunc` is deprecated. Use' + '`neurom.features.section` instead.') # pragma: no cover diff --git a/neurom/geom/transform.py b/neurom/geom/transform.py index e0467a8c8..726865eea 100644 --- a/neurom/geom/transform.py +++ b/neurom/geom/transform.py @@ -129,7 +129,7 @@ def rotate(obj, axis, angle, origin=None): """Rotation around unit vector following the right hand rule. Arguments: - obj : obj to be rotated (e.g. neurite, neuron). + obj : obj to be rotated (e.g. neurite, morphology). Must implement a transform method. axis : unit vector for the axis of rotation angle : rotation angle in rads diff --git a/neurom/io/utils.py b/neurom/io/utils.py index 9b033b63f..393d4f64a 100644 --- a/neurom/io/utils.py +++ b/neurom/io/utils.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Utility functions and for loading neurons.""" +"""Utility functions and for loading morphs.""" import logging import os @@ -38,9 +38,10 @@ from pathlib import Path import morphio -from neurom.core.neuron import Neuron +from neurom.core.morphology import Morphology from neurom.core.population import Population from neurom.exceptions import NeuroMError +from neurom.utils import warn_deprecated L = logging.getLogger(__name__) @@ -50,7 +51,7 @@ def _is_morphology_file(filepath): return filepath.is_file() and filepath.suffix.lower() in {'.swc', '.h5', '.asc'} -class NeuronLoader: +class MorphLoader: """Caching morphology loader. Arguments: @@ -60,7 +61,7 @@ class NeuronLoader: """ def __init__(self, directory, file_ext=None, cache_size=None): - """Initialize a NeuronLoader object.""" + """Initialize a MorphLoader object.""" self.directory = Path(directory) self.file_ext = file_ext if cache_size is not None: @@ -80,7 +81,7 @@ def _filepath(self, name): # pylint:disable=method-hidden def get(self, name): """Get `name` morphology data.""" - return load_neuron(self._filepath(name)) + return load_morphology(self._filepath(name)) def get_morph_files(directory): @@ -119,31 +120,29 @@ def _get_file(stream, extension): return temp_file -def load_neuron(neuron, reader=None): - """Build section trees from a neuron or a h5, swc or asc file. +def load_morphology(morph, reader=None): + """Build section trees from a morphology or a h5, swc or asc file. Args: - neuron (str|Path|Neuron|morphio.Morphology|morphio.mut.Morphology): A neuron representation - It can be: + morph (str|Path|Morphology|morphio.Morphology|morphio.mut.Morphology): a morphology + representation. It can be: - a filename with the h5, swc or asc extension - a NeuroM Neuron object - a morphio mutable or immutable Morphology object - a stream that can be put into a io.StreamIO object. In this case, the READER argument must be passed with the corresponding file format (asc, swc and h5) - reader (str): Optional, must be provided if neuron is a stream to + reader (str): Optional, must be provided if morphology is a stream to specify the file format (asc, swc, h5) Returns: - A Neuron object + A Morphology object Examples:: - neuron = neurom.load_neuron('my_neuron_file.h5') - - neuron = neurom.load_neuron(morphio.Morphology('my_neuron_file.h5')) - - neuron = nm.load_neuron(io.StringIO('''((Dendrite) + morphology = neurom.load_morphology('my_morphology_file.h5') + morphology = neurom.load_morphology(morphio.Morphology('my_morphology_file.h5')) + morphology = nm.load_morphology(io.StringIO('''((Dendrite) (3 -4 0 2) (3 -6 0 2) (3 -8 0 2) @@ -157,39 +156,53 @@ def load_neuron(neuron, reader=None): ) )'''), reader='asc') """ - if isinstance(neuron, (Neuron, morphio.Morphology, morphio.mut.Morphology)): - return Neuron(neuron) + if isinstance(morph, (Morphology, morphio.Morphology, morphio.mut.Morphology)): + return Morphology(morph) if reader: - return Neuron(_get_file(neuron, reader)) + return Morphology(_get_file(morph, reader)) + + return Morphology(morph, Path(morph).name) - return Neuron(neuron, Path(neuron).name) +def load_neuron(morph, reader=None): + """Deprecated in favor of ``load_morphology``.""" + warn_deprecated('`neurom.io.utils.load_neuron` is deprecated in favor of ' + '`neurom.io.utils.load_morphology`') # pragma: no cover + return load_morphology(morph, reader) # pragma: no cover -def load_neurons(neurons, - name=None, - ignored_exceptions=(), - cache=False): + +def load_morphologies(morphs, + name=None, + ignored_exceptions=(), + cache=False): """Create a population object. From all morphologies in a directory of from morphologies in a list of file names. Arguments: - neurons(str|Path|Iterable[Path]): path to a folder or list of paths to neuron files + morphs(str|Path|Iterable[Path]): path to a folder or list of paths to morphology files name (str): optional name of population. By default 'Population' or\ - filepath basename depending on whether neurons is list or\ + filepath basename depending on whether morphologies is list or\ directory path respectively. ignored_exceptions (tuple): NeuroM and MorphIO exceptions that you want to ignore when - loading neurons - cache (bool): whether to cache the loaded neurons in memory + loading morphologies + cache (bool): whether to cache the loaded morphologies in memory Returns: Population: population object """ - if isinstance(neurons, (str, Path)): - files = get_files_by_path(neurons) - name = name or Path(neurons).name + if isinstance(morphs, (str, Path)): + files = get_files_by_path(morphs) + name = name or Path(morphs).name else: - files = neurons + files = morphs name = name or 'Population' return Population(files, name, ignored_exceptions, cache) + + +def load_neurons(morphs, name=None, ignored_exceptions=(), cache=False): + """Deprecated in favor of ``load_morphologies``.""" + warn_deprecated('`neurom.io.utils.load_neurons` is deprecated in favor of ' + '`neurom.io.utils.load_morphologies`') # pragma: no cover + return load_morphologies(morphs, name, ignored_exceptions, cache) # pragma: no cover diff --git a/neurom/utils.py b/neurom/utils.py index ba78d626d..c9269fa14 100644 --- a/neurom/utils.py +++ b/neurom/utils.py @@ -30,56 +30,12 @@ import json import warnings from enum import Enum -from functools import partial, update_wrapper, wraps +from functools import wraps import numpy as np -class memoize: - """cache the return value of a method. - - This class is meant to be used as a decorator of methods. The return value - from a given method invocation will be cached on the instance whose method - was invoked. All arguments passed to a method decorated with memoize must - be hashable. - - If a memoized method is invoked directly on its class the result will not - be cached. Instead the method will be invoked like a static method:: - - class Obj: - @memoize - def add_to(self, arg): - return self + arg - - Obj.add_to(1) # not enough arguments - Obj.add_to(1, 2) # returns 3, result is not cached - """ - - def __init__(self, func): - """Initialize a memoize object.""" - self.func = func - update_wrapper(self, func) - - def __get__(self, obj, objtype=None): - """Get the attribute from the object.""" - return partial(self, obj) - - def __call__(self, *args, **kw): - """Callable for decorator.""" - obj = args[0] - try: - cache = obj.__cache # pylint: disable=protected-access - except AttributeError: - cache = obj.__cache = {} - key = (self.func, args[1:], frozenset(kw.items())) - try: - res = cache[key] - except KeyError: - res = cache[key] = self.func(*args, **kw) - return res - - -def _warn_deprecated(msg): +def warn_deprecated(msg): """Issue a deprecation warning.""" warnings.simplefilter('always', DeprecationWarning) warnings.warn(msg, category=DeprecationWarning, stacklevel=2) @@ -94,7 +50,7 @@ def _deprecated(fun): def _wrapper(*args, **kwargs): """Issue deprecation warning and forward arguments to fun.""" name = fun_name if fun_name is not None else fun.__name__ - _warn_deprecated('Call to deprecated function %s. %s' % (name, msg)) + warn_deprecated('Call to deprecated function %s. %s' % (name, msg)) return fun(*args, **kwargs) return _wrapper @@ -102,9 +58,9 @@ def _wrapper(*args, **kwargs): return _deprecated -def deprecated_module(mod_name, msg=""): +def deprecated_module(msg): """Issue a deprecation warning for a module.""" - _warn_deprecated('Module %s is deprecated. %s' % (mod_name, msg)) + warn_deprecated(msg) class NeuromJSON(json.JSONEncoder): diff --git a/neurom/view/__init__.py b/neurom/view/__init__.py index 2c0f3cdd3..87a4bcabb 100644 --- a/neurom/view/__init__.py +++ b/neurom/view/__init__.py @@ -27,7 +27,7 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """View tools to visualize morphologies.""" -from neurom.view.view import (plot_neuron, plot_neuron3d, - plot_tree, plot_tree3d, - plot_soma, plot_soma3d, - plot_dendrogram) +from neurom.view.matplotlib_impl import (plot_morph, plot_morph3d, + plot_tree, plot_tree3d, + plot_soma, plot_soma3d, + plot_dendrogram) diff --git a/neurom/view/dendrogram.py b/neurom/view/dendrogram.py index 1e46bf5e1..a416bb744 100644 --- a/neurom/view/dendrogram.py +++ b/neurom/view/dendrogram.py @@ -27,9 +27,10 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Dendrogram helper functions and class.""" + import numpy as np from neurom import NeuriteType -from neurom.core.neuron import Neurite, Neuron +from neurom.core.morphology import Neurite, Morphology from neurom.core.dataformat import COLS from neurom.morphmath import interval_lengths @@ -41,9 +42,9 @@ def __init__(self, neurom_section): """Dendrogram for NeuroM section tree. Args: - neurom_section (Neurite|Neuron|Section): tree to build dendrogram for. + neurom_section (Neurite|Morphology|Section): tree to build dendrogram for. """ - if isinstance(neurom_section, Neuron): + if isinstance(neurom_section, Morphology): self.neurite_type = NeuriteType.soma self.height = 1 self.width = 1 diff --git a/neurom/view/view.py b/neurom/view/matplotlib_impl.py similarity index 82% rename from neurom/view/view.py rename to neurom/view/matplotlib_impl.py index 316bbeab6..c113f4bc6 100644 --- a/neurom/view/view.py +++ b/neurom/view/matplotlib_impl.py @@ -25,21 +25,24 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Visualize morphologies.""" + +"""Morphology draw functions using matplotlib.""" + +from functools import wraps import numpy as np from matplotlib.collections import LineCollection, PatchCollection from matplotlib.lines import Line2D from matplotlib.patches import Circle, FancyArrowPatch, Polygon, Rectangle from mpl_toolkits.mplot3d.art3d import Line3DCollection from neurom import NeuriteType, geom -from neurom.core.neuron import iter_neurites, iter_sections, iter_segments +from neurom.core.morphology import iter_neurites, iter_sections, iter_segments from neurom.core.soma import SomaCylinders from neurom.core.dataformat import COLS from neurom.core.types import tree_type_checker from neurom.morphmath import segment_radius from neurom.view.dendrogram import Dendrogram, get_size, layout_dendrogram, move_positions -from neurom.view import common +from neurom.view import matplotlib_utils _LINEWIDTH = 1.2 _ALPHA = 0.8 @@ -57,6 +60,28 @@ NeuriteType.custom10: 'orange'} +def _implicit_ax(plot_func, params=None): + """Sets ``ax`` arg for plot functions if ``ax`` is not set originally.""" + + @wraps(plot_func) + def wrapper(*args, **kwargs): + fig = None + ax = kwargs.get('ax', None) + if ax is None and len(args) == 1: + fig, ax = matplotlib_utils.get_figure(params=params) + kwargs['ax'] = ax + res = plot_func(*args, **kwargs) + if fig: + matplotlib_utils.plot_style(fig=fig, ax=ax) + return res + + return wrapper + + +def _implicit_ax3d(plot_func): + return _implicit_ax(plot_func, {'projection': '3d'}) + + def _plane2col(plane): """Take a string like 'xy', and return the indices from COLS.*.""" planes = ('xy', 'yx', 'xz', 'zx', 'yz', 'zy') @@ -85,14 +110,15 @@ def _get_color(treecolor, tree_type): return TREE_COLOR.get(tree_type, 'green') -def plot_tree(ax, tree, plane='xy', +@_implicit_ax +def plot_tree(tree, ax=None, plane='xy', diameter_scale=_DIAMETER_SCALE, linewidth=_LINEWIDTH, color=None, alpha=_ALPHA, realistic_diameters=False): """Plots a 2d figure of the tree's segments. Args: - ax(matplotlib axes): on what to plot tree(neurom.core.Section or neurom.core.Neurite): plotted tree + ax(matplotlib axes): on what to plot plane(str): Any pair of 'xyz' diameter_scale(float): Scale factor multiplied with segment diameters before plotting linewidth(float): all segments are plotted with this width, but only if diameter_scale=None @@ -144,15 +170,16 @@ def _get_rectangle(x, y, linewidth): ax.add_collection(collection) -def plot_soma(ax, soma, plane='xy', +@_implicit_ax +def plot_soma(soma, ax=None, plane='xy', soma_outline=True, linewidth=_LINEWIDTH, color=None, alpha=_ALPHA): """Generates a 2d figure of the soma. Args: - ax(matplotlib axes): on what to plot soma(neurom.core.Soma): plotted soma + ax(matplotlib axes): on what to plot plane(str): Any pair of 'xyz' soma_outline(bool): should the soma be drawn as an outline linewidth(float): all segments are plotted with this width, but only if diameter_scale=None @@ -164,10 +191,11 @@ def plot_soma(ax, soma, plane='xy', if isinstance(soma, SomaCylinders): for start, end in zip(soma.points, soma.points[1:]): - common.project_cylinder_onto_2d(ax, (plane0, plane1), - start=start[COLS.XYZ], end=end[COLS.XYZ], - start_radius=start[COLS.R], end_radius=end[COLS.R], - color=color, alpha=alpha) + matplotlib_utils.project_cylinder_onto_2d( + ax, (plane0, plane1), + start=start[COLS.XYZ], end=end[COLS.XYZ], + start_radius=start[COLS.R], end_radius=end[COLS.R], + color=color, alpha=alpha) else: if soma_outline: ax.add_artist(Circle(soma.center[[plane0, plane1]], soma.radius, @@ -189,18 +217,19 @@ def plot_soma(ax, soma, plane='xy', # pylint: disable=too-many-arguments -def plot_neuron(ax, nrn, - neurite_type=NeuriteType.all, - plane='xy', - soma_outline=True, - diameter_scale=_DIAMETER_SCALE, linewidth=_LINEWIDTH, - color=None, alpha=_ALPHA, realistic_diameters=False): - """Plots a 2D figure of the neuron, that contains a soma and the neurites. +@_implicit_ax +def plot_morph(morph, ax=None, + neurite_type=NeuriteType.all, + plane='xy', + soma_outline=True, + diameter_scale=_DIAMETER_SCALE, linewidth=_LINEWIDTH, + color=None, alpha=_ALPHA, realistic_diameters=False): + """Plots a 2D figure of the morphology, that contains a soma and the neurites. Args: - ax(matplotlib axes): on what to plot neurite_type(NeuriteType|tuple): an optional filter on the neurite type - nrn(neuron): neuron to be plotted + ax(matplotlib axes): on what to plot + morph(Morphology): morphology to be plotted soma_outline(bool): should the soma be drawn as an outline plane(str): Any pair of 'xyz' diameter_scale(float): Scale factor multiplied with segment diameters before plotting @@ -209,15 +238,15 @@ def plot_neuron(ax, nrn, alpha(float): Transparency of plotted values realistic_diameters(bool): scale linewidths with axis data coordinates """ - plot_soma(ax, nrn.soma, plane=plane, soma_outline=soma_outline, linewidth=linewidth, + plot_soma(morph.soma, ax, plane=plane, soma_outline=soma_outline, linewidth=linewidth, color=color, alpha=alpha) - for neurite in iter_neurites(nrn, filt=tree_type_checker(neurite_type)): - plot_tree(ax, neurite, plane=plane, + for neurite in iter_neurites(morph, filt=tree_type_checker(neurite_type)): + plot_tree(neurite, ax, plane=plane, diameter_scale=diameter_scale, linewidth=linewidth, color=color, alpha=alpha, realistic_diameters=realistic_diameters) - ax.set_title(nrn.name) + ax.set_title(morph.name) ax.set_xlabel(plane[0]) ax.set_ylabel(plane[1]) @@ -234,7 +263,8 @@ def _update_3d_datalim(ax, obj): ax.zz_dataLim.update_from_data_xy(z_bounds, ignore=False) -def plot_tree3d(ax, tree, +@_implicit_ax3d +def plot_tree3d(tree, ax=None, diameter_scale=_DIAMETER_SCALE, linewidth=_LINEWIDTH, color=None, alpha=_ALPHA): """Generates a figure of the tree in 3d. @@ -243,8 +273,8 @@ def plot_tree3d(ax, tree, since no segments can be constructed. Args: - ax(matplotlib axes): on what to plot tree(neurom.core.Section or neurom.core.Neurite): plotted tree + ax(matplotlib axes): on what to plot diameter_scale(float): Scale factor multiplied with segment diameters before plotting linewidth(float): all segments are plotted with this width, but only if diameter_scale=None color(str or None): Color of plotted values, None corresponds to default choice @@ -264,12 +294,13 @@ def plot_tree3d(ax, tree, _update_3d_datalim(ax, tree) -def plot_soma3d(ax, soma, color=None, alpha=_ALPHA): +@_implicit_ax3d +def plot_soma3d(soma, ax=None, color=None, alpha=_ALPHA): """Generates a 3d figure of the soma. Args: - ax(matplotlib axes): on what to plot soma(neurom.core.Soma): plotted soma + ax(matplotlib axes): on what to plot color(str or None): Color of plotted values, None corresponds to default choice alpha(float): Transparency of plotted values """ @@ -277,40 +308,41 @@ def plot_soma3d(ax, soma, color=None, alpha=_ALPHA): if isinstance(soma, SomaCylinders): for start, end in zip(soma.points, soma.points[1:]): - common.plot_cylinder(ax, - start=start[COLS.XYZ], end=end[COLS.XYZ], - start_radius=start[COLS.R], end_radius=end[COLS.R], - color=color, alpha=alpha) + matplotlib_utils.plot_cylinder(ax, + start=start[COLS.XYZ], end=end[COLS.XYZ], + start_radius=start[COLS.R], end_radius=end[COLS.R], + color=color, alpha=alpha) else: - common.plot_sphere(ax, center=soma.center[COLS.XYZ], radius=soma.radius, - color=color, alpha=alpha) + matplotlib_utils.plot_sphere(ax, center=soma.center[COLS.XYZ], radius=soma.radius, + color=color, alpha=alpha) # unlike w/ 2d Axes, the dataLim isn't set by collections, so it has to be updated manually _update_3d_datalim(ax, soma) -def plot_neuron3d(ax, nrn, neurite_type=NeuriteType.all, - diameter_scale=_DIAMETER_SCALE, linewidth=_LINEWIDTH, - color=None, alpha=_ALPHA): - """Generates a figure of the neuron, that contains a soma and a list of trees. +@_implicit_ax3d +def plot_morph3d(morph, ax=None, neurite_type=NeuriteType.all, + diameter_scale=_DIAMETER_SCALE, linewidth=_LINEWIDTH, + color=None, alpha=_ALPHA): + """Generates a figure of the morphology, that contains a soma and a list of trees. Args: + morph(Morphology): morphology to be plotted ax(matplotlib axes): on what to plot - nrn(neuron): neuron to be plotted neurite_type(NeuriteType): an optional filter on the neurite type diameter_scale(float): Scale factor multiplied with segment diameters before plotting linewidth(float): all segments are plotted with this width, but only if diameter_scale=None color(str or None): Color of plotted values, None corresponds to default choice alpha(float): Transparency of plotted values """ - plot_soma3d(ax, nrn.soma, color=color, alpha=alpha) + plot_soma3d(morph.soma, ax, color=color, alpha=alpha) - for neurite in iter_neurites(nrn, filt=tree_type_checker(neurite_type)): - plot_tree3d(ax, neurite, + for neurite in iter_neurites(morph, filt=tree_type_checker(neurite_type)): + plot_tree3d(neurite, ax, diameter_scale=diameter_scale, linewidth=linewidth, color=color, alpha=alpha) - ax.set_title(nrn.name) + ax.set_title(morph.name) def _get_dendrogram_legend(dendrogram): @@ -365,12 +397,13 @@ def _get_dendrogram_shapes(dendrogram, positions, show_diameters): return shapes -def plot_dendrogram(ax, obj, show_diameters=True): +@_implicit_ax +def plot_dendrogram(obj, ax=None, show_diameters=True): """Plots Dendrogram of `obj`. Args: + obj (neurom.Morphology, neurom.Section): morphology or section ax: matplotlib axes - obj (neurom.Neuron, neurom.Section): neuron or section show_diameters (bool): whether to show node diameters or not """ dendrogram = Dendrogram(obj) diff --git a/neurom/view/common.py b/neurom/view/matplotlib_utils.py similarity index 99% rename from neurom/view/common.py rename to neurom/view/matplotlib_utils.py index 042e9982d..9d1314b12 100644 --- a/neurom/view/common.py +++ b/neurom/view/matplotlib_utils.py @@ -27,6 +27,7 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Functionality for styling plots.""" + from pathlib import Path import numpy as np diff --git a/neurom/view/plotly.py b/neurom/view/plotly.py deleted file mode 100644 index b73fbe0bc..000000000 --- a/neurom/view/plotly.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Define the public 'draw' function to be used to draw morphology using plotly.""" -from __future__ import absolute_import # prevents name clash with local plotly module -from itertools import chain - -import numpy as np - -try: - import plotly.graph_objs as go - from plotly.offline import plot, iplot, init_notebook_mode -except ImportError as e: - raise ImportError( - 'neurom[plotly] is not installed. Please install it by doing: pip install neurom[plotly]' - ) from e - -from neurom import COLS, iter_segments, iter_neurites -from neurom.core.neuron import Neuron -from neurom.view.view import TREE_COLOR - - -def draw(obj, plane='3d', inline=False, **kwargs): - """Draw the object using the given plane. - - obj (neurom.Neuron, neurom.Section): neuron or section - plane (str): a string representing the 2D plane (example: 'xy') - or '3d', '3D' for a 3D view - - inline (bool): must be set to True for interactive ipython notebook plotting - """ - if plane.lower() == '3d': - return _plot_neuron3d(obj, inline, **kwargs) - return _plot_neuron(obj, plane, inline, **kwargs) - - -def _plot_neuron(neuron, plane, inline, **kwargs): - return _plotly(neuron, plane=plane, title='neuron-2D', inline=inline, **kwargs) - - -def _plot_neuron3d(neuron, inline, **kwargs): - """Generates a figure of the neuron, that contains a soma and a list of trees.""" - return _plotly(neuron, plane='3d', title='neuron-3D', inline=inline, **kwargs) - - -def _make_trace(neuron, plane): - """Create the trace to be plotted.""" - for neurite in iter_neurites(neuron): - segments = list(iter_segments(neurite)) - - segs = [(s[0][COLS.XYZ], s[1][COLS.XYZ]) for s in segments] - - coords = dict(x=list(chain.from_iterable((p1[0], p2[0], None) for p1, p2 in segs)), - y=list(chain.from_iterable((p1[1], p2[1], None) for p1, p2 in segs)), - z=list(chain.from_iterable((p1[2], p2[2], None) for p1, p2 in segs))) - - color = TREE_COLOR.get(neurite.root_node.type, 'black') - if plane.lower() == '3d': - plot_fun = go.Scatter3d - else: - plot_fun = go.Scatter - coords = dict(x=coords[plane[0]], y=coords[plane[1]]) - yield plot_fun( - line=dict(color=color, width=2), - mode='lines', - **coords - ) - - -def _fill_soma_data(neuron, data, plane): - """Fill soma data if 3D plot and returns soma_2d in all cases.""" - if not isinstance(neuron, Neuron): - return [] - - if plane != '3d': - soma_2d = [ - # filled circle - { - 'type': 'circle', - 'xref': 'x', - 'yref': 'y', - 'fillcolor': 'rgba(50, 171, 96, 0.7)', - 'x0': neuron.soma.center[0] - neuron.soma.radius, - 'y0': neuron.soma.center[1] - neuron.soma.radius, - 'x1': neuron.soma.center[0] + neuron.soma.radius, - 'y1': neuron.soma.center[1] + neuron.soma.radius, - - 'line': { - 'color': 'rgba(50, 171, 96, 1)', - }, - }, - ] - - else: - soma_2d = [] - point_count = 100 # Enough points so that the surface looks like a sphere - theta = np.linspace(0, 2 * np.pi, point_count) - phi = np.linspace(0, np.pi, point_count) - r = neuron.soma.radius - data.append( - go.Surface( - x=r * np.outer(np.cos(theta), np.sin(phi)) + neuron.soma.center[0], - y=r * np.outer(np.sin(theta), np.sin(phi)) + neuron.soma.center[1], - z=r * np.outer(np.ones(point_count), np.cos(phi)) + neuron.soma.center[2], - cauto=False, - surfacecolor=['black'] * len(phi), - showscale=False, - ) - ) - return soma_2d - - -def get_figure(neuron, plane, title): - """Returns the plotly figure containing the neuron.""" - data = list(_make_trace(neuron, plane)) - axis = dict( - gridcolor='rgb(255, 255, 255)', - zerolinecolor='rgb(255, 255, 255)', - showbackground=True, - backgroundcolor='rgb(230, 230,230)' - ) - - soma_2d = _fill_soma_data(neuron, data, plane) - - layout = dict( - autosize=True, - title=title, - scene=dict( # This is used for 3D plots - xaxis=axis, yaxis=axis, zaxis=axis, - camera=dict(up=dict(x=0, y=0, z=1), eye=dict(x=-1.7428, y=1.0707, z=0.7100,)), - aspectmode='data' - ), - yaxis=dict(scaleanchor="x"), # This is used for 2D plots - shapes=soma_2d, - ) - - res = dict(data=data, layout=layout) - return res - - -def _plotly(neuron, plane, title, inline, **kwargs): - fig = get_figure(neuron, plane, title) - - plot_fun = iplot if inline else plot - if inline: - init_notebook_mode(connected=True) # pragma: no cover - plot_fun(fig, filename=title + '.html', **kwargs) - return fig diff --git a/neurom/view/plotly_impl.py b/neurom/view/plotly_impl.py new file mode 100644 index 000000000..d498c1b55 --- /dev/null +++ b/neurom/view/plotly_impl.py @@ -0,0 +1,173 @@ +# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 501ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Morphology draw functions using plotly.""" + +from itertools import chain + +import numpy as np + +try: + import plotly.graph_objs as go + from plotly.offline import plot, iplot, init_notebook_mode +except ImportError as e: + raise ImportError( + 'neurom[plotly] is not installed. Please install it by doing: pip install neurom[plotly]' + ) from e + +from neurom import COLS, iter_segments, iter_neurites +from neurom.core.morphology import Morphology +from neurom.view.matplotlib_impl import TREE_COLOR + + +def plot_morph(morph, plane='xy', inline=False, **kwargs): + """Draw morphology in 2D. + + Args: + morph(Morphology|Section): morphology or section + plane(str): a string representing the 2D plane (example: 'xy') + inline(bool): must be set to True for interactive ipython notebook plotting + **kwargs: additional plotly keyword arguments + """ + return _plotly(morph, plane=plane, title='morphology-2D', inline=inline, **kwargs) + + +def plot_morph3d(morph, inline=False, **kwargs): + """Draw morphology in 3D. + + Args: + morph(Morphology|Section): morphology or section + inline(bool): must be set to True for interactive ipython notebook plotting + **kwargs: additional plotly keyword arguments + """ + return _plotly(morph, plane='3d', title='morphology-3D', inline=inline, **kwargs) + + +def _make_trace(morph, plane): + """Create the trace to be plotted.""" + for neurite in iter_neurites(morph): + segments = list(iter_segments(neurite)) + + segs = [(s[0][COLS.XYZ], s[1][COLS.XYZ]) for s in segments] + + coords = dict(x=list(chain.from_iterable((p1[0], p2[0], None) for p1, p2 in segs)), + y=list(chain.from_iterable((p1[1], p2[1], None) for p1, p2 in segs)), + z=list(chain.from_iterable((p1[2], p2[2], None) for p1, p2 in segs))) + + color = TREE_COLOR.get(neurite.root_node.type, 'black') + if plane.lower() == '3d': + plot_fun = go.Scatter3d + else: + plot_fun = go.Scatter + coords = dict(x=coords[plane[0]], y=coords[plane[1]]) + yield plot_fun( + line=dict(color=color, width=2), + mode='lines', + **coords + ) + + +def _fill_soma_data(morph, data, plane): + """Fill soma data if 3D plot and returns soma_2d in all cases.""" + if not isinstance(morph, Morphology): + return [] + + if plane != '3d': + soma_2d = [ + # filled circle + { + 'type': 'circle', + 'xref': 'x', + 'yref': 'y', + 'fillcolor': 'rgba(50, 171, 96, 0.7)', + 'x0': morph.soma.center[0] - morph.soma.radius, + 'y0': morph.soma.center[1] - morph.soma.radius, + 'x1': morph.soma.center[0] + morph.soma.radius, + 'y1': morph.soma.center[1] + morph.soma.radius, + + 'line': { + 'color': 'rgba(50, 171, 96, 1)', + }, + }, + ] + + else: + soma_2d = [] + point_count = 100 # Enough points so that the surface looks like a sphere + theta = np.linspace(0, 2 * np.pi, point_count) + phi = np.linspace(0, np.pi, point_count) + r = morph.soma.radius + data.append( + go.Surface( + x=r * np.outer(np.cos(theta), np.sin(phi)) + morph.soma.center[0], + y=r * np.outer(np.sin(theta), np.sin(phi)) + morph.soma.center[1], + z=r * np.outer(np.ones(point_count), np.cos(phi)) + morph.soma.center[2], + cauto=False, + surfacecolor=['black'] * len(phi), + showscale=False, + ) + ) + return soma_2d + + +def get_figure(morph, plane, title): + """Returns the plotly figure containing the morphology.""" + data = list(_make_trace(morph, plane)) + axis = dict( + gridcolor='rgb(255, 255, 255)', + zerolinecolor='rgb(255, 255, 255)', + showbackground=True, + backgroundcolor='rgb(230, 230,230)' + ) + + soma_2d = _fill_soma_data(morph, data, plane) + + layout = dict( + autosize=True, + title=title, + scene=dict( # This is used for 3D plots + xaxis=axis, yaxis=axis, zaxis=axis, + camera=dict(up=dict(x=0, y=0, z=1), eye=dict(x=-1.7428, y=1.0707, z=0.7100,)), + aspectmode='data' + ), + yaxis=dict(scaleanchor="x"), # This is used for 2D plots + shapes=soma_2d, + ) + + res = dict(data=data, layout=layout) + return res + + +def _plotly(morph, plane, title, inline, **kwargs): + fig = get_figure(morph, plane, title) + + plot_fun = iplot if inline else plot + if inline: + init_notebook_mode(connected=True) # pragma: no cover + plot_fun(fig, filename=title + '.html', **kwargs) + return fig diff --git a/neurom/viewer.py b/neurom/viewer.py index 363d0835f..3faadc62b 100644 --- a/neurom/viewer.py +++ b/neurom/viewer.py @@ -31,26 +31,29 @@ Examples: >>> from neurom import viewer - >>> nrn = ... # load a neuron - >>> viewer.draw(nrn) # 2d plot - >>> viewer.draw(nrn, mode='3d') # 3d plot - >>> viewer.draw(nrn.neurites[0]) # 2d plot of neurite tree - >>> viewer.draw(nrn, mode='dendrogram') # dendrogram plot + >>> m = ... # load a neuron + >>> viewer.draw(m) # 2d plot + >>> viewer.draw(m, mode='3d') # 3d plot + >>> viewer.draw(m.neurites[0]) # 2d plot of neurite tree + >>> viewer.draw(m, mode='dendrogram') # dendrogram plot """ -from neurom.view.view import (plot_neuron, plot_neuron3d, - plot_tree, plot_tree3d, - plot_soma, plot_soma3d, - plot_dendrogram) -from neurom.view import common -from neurom.core.neuron import Section, Neurite, Neuron +from neurom.view.matplotlib_impl import (plot_morph, plot_morph3d, + plot_tree, plot_tree3d, + plot_soma, plot_soma3d, + plot_dendrogram) +from neurom.view import matplotlib_utils +from neurom.core.morphology import Section, Neurite, Morphology from neurom.core.soma import Soma +from neurom.utils import deprecated_module + +deprecated_module('Module `viewer` is deprecated. See the documentation\'s migration page.') MODES = ('2d', '3d', 'dendrogram') _VIEWERS = { - 'neuron_3d': plot_neuron3d, - 'neuron_2d': plot_neuron, + 'neuron_3d': plot_morph3d, + 'neuron_2d': plot_morph, 'neuron_dendrogram': plot_dendrogram, 'tree_3d': plot_tree3d, 'tree_2d': plot_tree, @@ -86,13 +89,14 @@ def draw(obj, mode='2d', **kwargs): NotDrawableError if obj type and mode combination is not drawable Examples: - >>> nrn = ... # load a neuron - >>> fig, _ = viewer.draw(nrn) # 2d plot + >>> from neurom import viewer, load_morphology + >>> m = load_morphology('/path/to/morphology') # load a neuron + >>> fig, _ = viewer.draw(m) # 2d plot >>> fig.show() - >>> fig3d, _ = viewer.draw(nrn, mode='3d') # 3d plot + >>> fig3d, _ = viewer.draw(m, mode='3d') # 3d plot >>> fig3d.show() - >>> fig, _ = viewer.draw(nrn.neurites[0]) # 2d plot of neurite tree - >>> dend, _ = viewer.draw(nrn, mode='dendrogram') + >>> fig, _ = viewer.draw(m.neurites[0]) # 2d plot of neurite tree + >>> dend, _ = viewer.draw(m, mode='dendrogram') """ if mode not in MODES: raise InvalidDrawModeError('Invalid drawing mode %s' % mode) @@ -102,10 +106,10 @@ def draw(obj, mode='2d', **kwargs): raise NotImplementedError('Option realistic_diameter not implemented for 3D plots') del kwargs['realistic_diameters'] - fig, ax = (common.get_figure() if mode in ('2d', 'dendrogram') - else common.get_figure(params={'projection': '3d'})) + fig, ax = (matplotlib_utils.get_figure() if mode in ('2d', 'dendrogram') + else matplotlib_utils.get_figure(params={'projection': '3d'})) - if isinstance(obj, Neuron): + if isinstance(obj, Morphology): tag = 'neuron' elif isinstance(obj, (Section, Neurite)): tag = 'tree' @@ -121,12 +125,12 @@ def draw(obj, mode='2d', **kwargs): raise NotDrawableError('No drawer for class %s, mode=%s' % (obj.__class__, mode)) from e output_path = kwargs.pop('output_path', None) - plotter(ax, obj, **kwargs) + plotter(obj, ax, **kwargs) if mode != 'dendrogram': - common.plot_style(fig=fig, ax=ax, **kwargs) + matplotlib_utils.plot_style(fig=fig, ax=ax, **kwargs) if output_path: - common.save_plot(fig=fig, output_path=output_path, **kwargs) + matplotlib_utils.save_plot(fig=fig, output_path=output_path, **kwargs) return fig, ax diff --git a/pylintrc b/pylintrc index 1a170cc5b..ddc8a7423 100644 --- a/pylintrc +++ b/pylintrc @@ -6,7 +6,7 @@ #R0903 - Too Few public methods #W0511 - TODO in code #R0401 - cyclic-import -disable=C0103,R0903,W0511,R0401 +disable=C0103,R0903,W0511,R0401,unspecified-encoding [FORMAT] # Maximum number of characters on a single line. diff --git a/setup.py b/setup.py index ea100d245..fee29bb7f 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ 'scipy>=1.2.0', 'tqdm>=4.8.4', ], - packages=find_packages(), + packages=find_packages(exclude=('tests',)), license='BSD', entry_points={ 'console_scripts': ['neurom=neurom.apps.cli:cli'] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/apps/test_annotate.py b/tests/apps/test_annotate.py index f2f4b6cff..49a5cbfff 100644 --- a/tests/apps/test_annotate.py +++ b/tests/apps/test_annotate.py @@ -1,8 +1,8 @@ from pathlib import Path -from neurom import load_neuron +from neurom import load_morphology from neurom.apps.annotate import annotate, generate_annotation from neurom.check import CheckResult -from neurom.check.neuron_checks import has_no_narrow_start +from neurom.check.morphology_checks import has_no_narrow_start SWC_PATH = Path(__file__).parent.parent / 'data/swc' @@ -45,6 +45,6 @@ def test_annotate(): "label": "Circle1", "color": "Blue"}} - neuron = load_neuron(SWC_PATH / 'narrow_start.swc') - results = [checker(neuron) for checker in checkers.keys()] + m = load_morphology(SWC_PATH / 'narrow_start.swc') + results = [checker(m) for checker in checkers.keys()] assert annotate(results, checkers.values()) == correct_result diff --git a/tests/apps/test_cli.py b/tests/apps/test_cli.py index f334c118e..763ebb79d 100644 --- a/tests/apps/test_cli.py +++ b/tests/apps/test_cli.py @@ -22,18 +22,23 @@ def test_viewer_matplotlib(mock): assert result.exit_code == 0 mock.assert_called_once() + mock.reset_mock() + result = runner.invoke(cli, ['view', filename, '--3d']) + assert result.exit_code == 0 + mock.assert_called_once() + mock.reset_mock() result = runner.invoke(cli, ['view', filename, '--plane', 'xy']) assert result.exit_code == 0 mock.assert_called_once() -@patch('neurom.view.plotly.plot') +@patch('neurom.view.plotly_impl.plot') def test_viewer_plotly(mock): runner = CliRunner() filename = str(DATA / 'swc' / 'simple.swc') - result = runner.invoke(cli, ['view', filename, + result = runner.invoke(cli, ['view', filename, '--3d', '--backend', 'plotly']) assert result.exit_code == 0 mock.assert_called_once() @@ -53,26 +58,26 @@ def test_morph_stat(): result = runner.invoke(cli, ['stats', str(filename), '--output', f.name]) assert result.exit_code == 0 df = pd.read_csv(f) - assert set(df.columns) == {'name', 'axon:max_section_length', 'axon:total_section_length', - 'axon:total_section_volume', 'axon:max_section_branch_order', - 'apical_dendrite:max_section_length', - 'apical_dendrite:total_section_length', - 'apical_dendrite:total_section_volume', - 'apical_dendrite:max_section_branch_order', - 'basal_dendrite:max_section_length', - 'basal_dendrite:total_section_length', - 'basal_dendrite:total_section_volume', - 'basal_dendrite:max_section_branch_order', - 'all:max_section_length', - 'all:total_section_length', 'all:total_section_volume', - 'all:max_section_branch_order', 'neuron:mean_soma_radius'} + assert set(df.columns) == {'name', 'axon:max_section_lengths', 'axon:sum_section_lengths', + 'axon:sum_section_volumes', 'axon:max_section_branch_orders', + 'apical_dendrite:max_section_lengths', + 'apical_dendrite:sum_section_lengths', + 'apical_dendrite:sum_section_volumes', + 'apical_dendrite:max_section_branch_orders', + 'basal_dendrite:max_section_lengths', + 'basal_dendrite:sum_section_lengths', + 'basal_dendrite:sum_section_volumes', + 'basal_dendrite:max_section_branch_orders', + 'all:max_section_lengths', + 'all:sum_section_lengths', 'all:sum_section_volumes', + 'all:max_section_branch_orders', 'morphology:mean_soma_radius'} def test_morph_stat_full_config(): runner = CliRunner() filename = DATA / 'h5/v1/Neuron.h5' with tempfile.NamedTemporaryFile() as f: - result = runner.invoke(cli, ['stats', str(filename), '--full-config', '--output', f.name], catch_exceptions=False) + result = runner.invoke(cli, ['stats', str(filename), '--full-config', '--output', f.name]) assert result.exit_code == 0 df = pd.read_csv(f) assert not df.empty @@ -111,7 +116,6 @@ def test_morph_stat_json(): assert content - def test_morph_check(): runner = CliRunner() filename = DATA / 'swc' / 'simple.swc' diff --git a/tests/apps/test_config.py b/tests/apps/test_config.py index a9bdaf1a0..9e44cc9d9 100644 --- a/tests/apps/test_config.py +++ b/tests/apps/test_config.py @@ -39,7 +39,7 @@ def test_get_config(): test_yaml = Path(__file__).parent.parent.parent / 'neurom/apps/config/morph_stats.yaml' - expected = {'neurite': {'section_lengths': ['max', 'total'], 'section_volumes': ['total'], 'section_branch_orders': ['max']}, 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], 'neuron': {'soma_radii': ['mean']}} + expected = {'neurite': {'section_lengths': ['max', 'sum'], 'section_volumes': ['sum'], 'section_branch_orders': ['max']}, 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], 'morphology': {'soma_radius': ['mean']}} config = get_config(None, test_yaml) assert config == expected diff --git a/tests/apps/test_morph_stats.py b/tests/apps/test_morph_stats.py index 68f209120..33e97a44c 100644 --- a/tests/apps/test_morph_stats.py +++ b/tests/apps/test_morph_stats.py @@ -31,11 +31,10 @@ from pathlib import Path import neurom as nm -import numpy as np import pandas as pd from neurom.apps import morph_stats as ms from neurom.exceptions import ConfigError -from neurom.features import NEURITEFEATURES, NEURONFEATURES +from neurom.features import _NEURITE_FEATURES, _MORPHOLOGY_FEATURES, _POPULATION_FEATURES import pytest from numpy.testing import assert_array_equal, assert_almost_equal @@ -45,206 +44,203 @@ SWC_PATH = DATA_PATH / 'swc' REF_CONFIG = { 'neurite': { - 'section_lengths': ['max', 'total'], - 'section_volumes': ['total'], + 'section_lengths': ['max', 'sum'], + 'section_volumes': ['sum'], 'section_branch_orders': ['max'], 'segment_midpoints': ['max'], 'max_radial_distance': ['mean'], }, 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], - 'neuron': { - 'soma_radii': ['mean'], + 'morphology': { + 'soma_radius': ['mean'], 'max_radial_distance': ['mean'], } } +REF_CONFIG_NEW = { + 'neurite': { + 'section_lengths': {'modes': ['max', 'sum']}, + 'section_volumes': {'modes': ['sum']}, + 'section_branch_orders': {'modes': ['max']}, + 'segment_midpoints': {'modes': ['max']}, + 'max_radial_distance': {'modes': ['mean']}, + }, + 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], + 'morphology': { + 'soma_radius': {'modes': ['mean']}, + 'max_radial_distance': {'modes': ['mean']}, + } +} + REF_OUT = { - 'neuron': { + 'morphology': { 'mean_soma_radius': 0.13065629648763766, 'mean_max_radial_distance': 99.5894610648815, }, 'axon': { - 'total_section_length': 207.87975220908129, - 'max_section_length': 11.018460736176685, - 'max_section_branch_order': 10, - 'total_section_volume': 276.73857657289523, - 'max_segment_midpoint_0': 0.0, - 'max_segment_midpoint_1': 0.0, - 'max_segment_midpoint_2': 49.520305964149998, + 'sum_section_lengths': 207.87975220908129, + 'max_section_lengths': 11.018460736176685, + 'max_section_branch_orders': 10, + 'sum_section_volumes': 276.73857657289523, + 'max_segment_midpoints_0': 0.0, + 'max_segment_midpoints_1': 0.0, + 'max_segment_midpoints_2': 49.520305964149998, 'mean_max_radial_distance': 82.44254511788921, }, 'all': { - 'total_section_length': 840.68521442251949, - 'max_section_length': 11.758281556059444, - 'max_section_branch_order': 10, - 'total_section_volume': 1104.9077419665782, - 'max_segment_midpoint_0': 64.401674984050004, - 'max_segment_midpoint_1': 48.48197694465, - 'max_segment_midpoint_2': 53.750947521650005, + 'sum_section_lengths': 840.68521442251949, + 'max_section_lengths': 11.758281556059444, + 'max_section_branch_orders': 10, + 'sum_section_volumes': 1104.9077419665782, + 'max_segment_midpoints_0': 64.401674984050004, + 'max_segment_midpoints_1': 48.48197694465, + 'max_segment_midpoints_2': 53.750947521650005, 'mean_max_radial_distance': 99.5894610648815, }, 'apical_dendrite': { - 'total_section_length': 214.37304577550353, - 'max_section_length': 11.758281556059444, - 'max_section_branch_order': 10, - 'total_section_volume': 271.9412385728449, - 'max_segment_midpoint_0': 64.401674984050004, - 'max_segment_midpoint_1': 0.0, - 'max_segment_midpoint_2': 53.750947521650005, + 'sum_section_lengths': 214.37304577550353, + 'max_section_lengths': 11.758281556059444, + 'max_section_branch_orders': 10, + 'sum_section_volumes': 271.9412385728449, + 'max_segment_midpoints_0': 64.401674984050004, + 'max_segment_midpoints_1': 0.0, + 'max_segment_midpoints_2': 53.750947521650005, 'mean_max_radial_distance': 99.5894610648815, }, 'basal_dendrite': { - 'total_section_length': 418.43241643793476, - 'max_section_length': 11.652508126101711, - 'max_section_branch_order': 10, - 'total_section_volume': 556.22792682083821, - 'max_segment_midpoint_0': 64.007872333250006, - 'max_segment_midpoint_1': 48.48197694465, - 'max_segment_midpoint_2': 51.575580778049996, + 'sum_section_lengths': 418.43241643793476, + 'max_section_lengths': 11.652508126101711, + 'max_section_branch_orders': 10, + 'sum_section_volumes': 556.22792682083821, + 'max_segment_midpoints_0': 64.007872333250006, + 'max_segment_midpoints_1': 48.48197694465, + 'max_segment_midpoints_2': 51.575580778049996, 'mean_max_radial_distance': 94.43342438865741, }, } -def test_name_correction(): - assert ms._stat_name('foo', 'raw') == 'foo' - assert ms._stat_name('foos', 'raw') == 'foo' - assert ms._stat_name('foos', 'bar') == 'bar_foo' - assert ms._stat_name('foos', 'total') == 'total_foo' - assert ms._stat_name('soma_radii', 'total') == 'total_soma_radius' - assert ms._stat_name('soma_radii', 'raw') == 'soma_radius' - - -def test_eval_stats_raw_returns_list(): - assert ms.eval_stats(np.array([1, 2, 3, 4]), 'raw') == [1, 2, 3, 4] - - -def test_eval_stats_empty_input_returns_none(): - assert ms.eval_stats([], 'min') is None - - -def test_eval_stats_total_returns_sum(): - assert ms.eval_stats(np.array([1, 2, 3, 4]), 'total') == 10 - - -def test_eval_stats_on_empty_stat(): - assert ms.eval_stats(np.array([]), 'mean') == None - assert ms.eval_stats(np.array([]), 'std') == None - assert ms.eval_stats(np.array([]), 'median') == None - assert ms.eval_stats(np.array([]), 'min') == None - assert ms.eval_stats(np.array([]), 'max') == None - - assert ms.eval_stats(np.array([]), 'raw') == [] - assert ms.eval_stats(np.array([]), 'total') == 0.0 - - -def test_eval_stats_applies_numpy_function(): - modes = ('min', 'max', 'mean', 'median', 'std') - - ref_array = np.arange(1, 10) - - for m in modes: - assert (ms.eval_stats(ref_array, m) == getattr(np, m)(ref_array)) +def test_extract_stats_single_morphology(): + m = nm.load_morphology(SWC_PATH / 'Neuron.swc') + res = ms.extract_stats(m, REF_CONFIG) + assert set(res.keys()) == set(REF_OUT.keys()) + for k in ('morphology', 'all', 'axon', 'basal_dendrite', 'apical_dendrite'): + assert set(res[k].keys()) == set(REF_OUT[k].keys()) + for kk in res[k].keys(): + assert_almost_equal(res[k][kk], REF_OUT[k][kk], decimal=4) -def test_extract_stats_single_neuron(): - nrn = nm.load_neuron(SWC_PATH / 'Neuron.swc') - res = ms.extract_stats(nrn, REF_CONFIG) +def test_extract_stats_new_format(): + m = nm.load_morphology(SWC_PATH / 'Neuron.swc') + res = ms.extract_stats(m, REF_CONFIG_NEW) assert set(res.keys()) == set(REF_OUT.keys()) - for k in ('neuron', 'all', 'axon', 'basal_dendrite', 'apical_dendrite'): + for k in ('morphology', 'all', 'axon', 'basal_dendrite', 'apical_dendrite'): assert set(res[k].keys()) == set(REF_OUT[k].keys()) for kk in res[k].keys(): assert_almost_equal(res[k][kk], REF_OUT[k][kk], decimal=4) +def test_stats_new_format_set_arg(): + m = nm.load_morphology(SWC_PATH / 'Neuron.swc') + config = { + 'neurite': { + 'section_lengths': {'kwargs': {'neurite_type': 'AXON'}, 'modes': ['max', 'sum']}, + }, + 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], + 'morphology': { + 'soma_radius': {'modes': ['mean']}, + } + } + + res = ms.extract_stats(m, config) + assert set(res.keys()) == {'morphology', 'axon'} + assert set(res['axon'].keys()) == {'max_section_lengths', 'sum_section_lengths'} + assert set(res['morphology'].keys()) == {'mean_soma_radius'} + + def test_extract_stats_scalar_feature(): - nrn = nm.load_neuron(DATA_PATH / 'neurolucida' / 'bio_neuron-000.asc') + m = nm.load_morphology(DATA_PATH / 'neurolucida' / 'bio_neuron-000.asc') config = { 'neurite_type': ['ALL'], 'neurite': { - 'n_forking_points': ['max'], + 'number_of_forking_points': ['max'], }, - 'neuron': { - 'soma_volume': ['total'], + 'morphology': { + 'soma_volume': ['sum'], } } - res = ms.extract_stats(nrn, config) - assert res == {'all': {'max_n_forking_point': 277}, - 'neuron': {'total_soma_volume': 1424.4383771584492}} + res = ms.extract_stats(m, config) + assert res == {'all': {'max_number_of_forking_points': 277}, + 'morphology': {'sum_soma_volume': 1424.4383771584492}} def test_extract_dataframe(): # Vanilla test - nrns = nm.load_neurons([SWC_PATH / name - for name in ['Neuron.swc', 'simple.swc']]) - actual = ms.extract_dataframe(nrns, REF_CONFIG) + morphs = nm.load_morphologies([SWC_PATH / 'Neuron.swc', SWC_PATH / 'simple.swc']) + actual = ms.extract_dataframe(morphs, REF_CONFIG_NEW) expected = pd.read_csv(Path(DATA_PATH, 'extracted-stats.csv'), header=[0, 1], index_col=0) - assert_frame_equal(actual, expected) + assert_frame_equal(actual, expected, check_dtype=False) - # Test with a single neuron in the population - nrns = nm.load_neurons(SWC_PATH / 'Neuron.swc') - actual = ms.extract_dataframe(nrns, REF_CONFIG) + # Test with a single morphology in the population + morphs = nm.load_morphologies(SWC_PATH / 'Neuron.swc') + actual = ms.extract_dataframe(morphs, REF_CONFIG_NEW) assert_frame_equal(actual, expected.iloc[[0]], check_dtype=False) - # Test with a config without the 'neuron' key - nrns = nm.load_neurons([Path(SWC_PATH, name) - for name in ['Neuron.swc', 'simple.swc']]) - config = {'neurite': {'section_lengths': ['total']}, + # Test with a config without the 'morphology' key + morphs = nm.load_morphologies([Path(SWC_PATH, name) + for name in ['Neuron.swc', 'simple.swc']]) + config = {'neurite': {'section_lengths': ['sum']}, 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL']} - actual = ms.extract_dataframe(nrns, config) + actual = ms.extract_dataframe(morphs, config) idx = pd.IndexSlice - expected = expected.loc[:, idx[:, ['name', 'total_section_length']]] - assert_frame_equal(actual, expected) + expected = expected.loc[:, idx[:, ['name', 'sum_section_lengths']]] + assert_frame_equal(actual, expected, check_dtype=False) - # Test with a FstNeuron argument - nrn = nm.load_neuron(Path(SWC_PATH, 'Neuron.swc')) - actual = ms.extract_dataframe(nrn, config) + # Test with a Morphology argument + m = nm.load_morphology(Path(SWC_PATH, 'Neuron.swc')) + actual = ms.extract_dataframe(m, config) assert_frame_equal(actual, expected.iloc[[0]], check_dtype=False) - # Test with a List[FstNeuron] argument - nrns = [nm.load_neuron(Path(SWC_PATH, name)) + # Test with a List[Morphology] argument + morphs = [nm.load_morphology(Path(SWC_PATH, name)) for name in ['Neuron.swc', 'simple.swc']] - actual = ms.extract_dataframe(nrns, config) - assert_frame_equal(actual, expected) + actual = ms.extract_dataframe(morphs, config) + assert_frame_equal(actual, expected, check_dtype=False) # Test with a List[Path] argument - nrns = [Path(SWC_PATH, name) for name in ['Neuron.swc', 'simple.swc']] - actual = ms.extract_dataframe(nrns, config) - assert_frame_equal(actual, expected) + morphs = [Path(SWC_PATH, name) for name in ['Neuron.swc', 'simple.swc']] + actual = ms.extract_dataframe(morphs, config) + assert_frame_equal(actual, expected, check_dtype=False) # Test without any neurite_type keys, it should pick the defaults - config = {'neurite': {'total_length_per_neurite': ['total']}} - actual = ms.extract_dataframe(nrns, config) + config = {'neurite': {'total_length_per_neurite': ['sum']}} + actual = ms.extract_dataframe(morphs, config) expected_columns = pd.MultiIndex.from_tuples( [('property', 'name'), - ('axon', 'total_total_length_per_neurite'), - ('basal_dendrite', 'total_total_length_per_neurite'), - ('apical_dendrite', 'total_total_length_per_neurite'), - ('all', 'total_total_length_per_neurite')]) + ('axon', 'sum_total_length_per_neurite'), + ('basal_dendrite', 'sum_total_length_per_neurite'), + ('apical_dendrite', 'sum_total_length_per_neurite'), + ('all', 'sum_total_length_per_neurite')]) expected = pd.DataFrame( columns=expected_columns, data=[['Neuron.swc', 207.87975221, 418.43241644, 214.37304578, 840.68521442], ['simple.swc', 15., 16., 0., 31., ]]) - assert_frame_equal(actual, expected) + assert_frame_equal(actual, expected, check_dtype=False) def test_extract_dataframe_multiproc(): - # FIXME: Cannot use Neuron objects in the extract_dataframe ctor right now - # because of "TypeError: can't pickle Neuron objects" - # nrns = nm.load_neurons([Path(SWC_PATH, name) - # for name in ['Neuron.swc', 'simple.swc']]) - nrns = [Path(SWC_PATH, name) + morphs = [Path(SWC_PATH, name) for name in ['Neuron.swc', 'simple.swc']] with warnings.catch_warnings(record=True) as w: - actual = ms.extract_dataframe(nrns, REF_CONFIG, n_workers=2) + actual = ms.extract_dataframe(morphs, REF_CONFIG, n_workers=2) expected = pd.read_csv(Path(DATA_PATH, 'extracted-stats.csv'), index_col=0, header=[0, 1]) - assert_frame_equal(actual, expected) + assert_frame_equal(actual, expected, check_dtype=False) with warnings.catch_warnings(record=True) as w: - actual = ms.extract_dataframe(nrns, REF_CONFIG, n_workers=os.cpu_count() + 1) + actual = ms.extract_dataframe(morphs, REF_CONFIG, n_workers=os.cpu_count() + 1) assert len(w) == 1, "Warning not emitted" - assert_frame_equal(actual, expected) + assert_frame_equal(actual, expected, check_dtype=False) def test_get_header(): @@ -255,7 +251,7 @@ def test_get_header(): header = ms.get_header(fake_results) assert 1 + 2 + 4 * (4 + 4) == len(header) # name + everything in REF_OUT assert 'name' in header - assert 'neuron:mean_soma_radius' in header + assert 'morphology:mean_soma_radius' in header def test_generate_flattened_dict(): @@ -271,10 +267,11 @@ def test_generate_flattened_dict(): def test_full_config(): config = ms.full_config() - assert set(config.keys()) == {'neurite', 'neuron', 'neurite_type'} + assert set(config.keys()) == {'neurite', 'population', 'morphology', 'neurite_type'} - assert set(config['neurite'].keys()) == set(NEURITEFEATURES.keys()) - assert set(config['neuron'].keys()) == set(NEURONFEATURES.keys()) + assert set(config['neurite'].keys()) == set(_NEURITE_FEATURES.keys()) + assert set(config['morphology'].keys()) == set(_MORPHOLOGY_FEATURES.keys()) + assert set(config['population'].keys()) == set(_POPULATION_FEATURES.keys()) def test_sanitize_config(): @@ -283,21 +280,21 @@ def test_sanitize_config(): ms.sanitize_config({'neurite': []}) new_config = ms.sanitize_config({}) # empty - assert 2 == len(new_config) # neurite & neuron created + assert 2 == len(new_config) # neurite & morphology created full_config = { 'neurite': { - 'section_lengths': ['max', 'total'], - 'section_volumes': ['total'], + 'section_lengths': ['max', 'sum'], + 'section_volumes': ['sum'], 'section_branch_orders': ['max'] }, 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], - 'neuron': { - 'soma_radii': ['mean'] + 'morphology': { + 'soma_radius': ['mean'] } } new_config = ms.sanitize_config(full_config) - assert 3 == len(new_config) # neurite, neurite_type & neuron + assert 3 == len(new_config) # neurite, neurite_type & morphology def test_multidimensional_features(): @@ -306,23 +303,23 @@ def test_multidimensional_features(): This should be the case even when the feature is `None` or `[]` - The following neuron has no axon but the axon feature segment_midpoints for + The following morphology has no axon but the axon feature segment_midpoints for the axon should still be made of 3 values (X, Y and Z) Cf: https://github.com/BlueBrain/NeuroM/issues/859 """ - neuron = nm.load_neuron(Path(SWC_PATH, 'no-axon.swc')) + m = nm.load_morphology(Path(SWC_PATH, 'no-axon.swc')) config = {'neurite': {'segment_midpoints': ['max']}, 'neurite_type': ['AXON']} - actual = ms.extract_dataframe(neuron, config) - assert_array_equal(actual['axon'][['max_segment_midpoint_0', - 'max_segment_midpoint_1', - 'max_segment_midpoint_2']].values, + actual = ms.extract_dataframe(m, config) + assert_array_equal(actual['axon'][['max_segment_midpoints_0', + 'max_segment_midpoints_1', + 'max_segment_midpoints_2']].values, [[None, None, None]]) config = {'neurite': {'partition_pairs': ['max']}} - actual = ms.extract_dataframe(neuron, config) - assert_array_equal(actual['axon'][['max_partition_pair_0', - 'max_partition_pair_1']].values, + actual = ms.extract_dataframe(m, config) + assert_array_equal(actual['axon'][['max_partition_pairs_0', + 'max_partition_pairs_1']].values, [[None, None]]) diff --git a/tests/check/test_neuron_checks.py b/tests/check/test_morphology_checks.py similarity index 62% rename from tests/check/test_neuron_checks.py rename to tests/check/test_morphology_checks.py index 97320f25a..8e15b50e8 100644 --- a/tests/check/test_neuron_checks.py +++ b/tests/check/test_morphology_checks.py @@ -30,8 +30,8 @@ from io import StringIO from pathlib import Path -from neurom import check, load_neuron -from neurom.check import neuron_checks as nrn_chk +from neurom import check, load_morphology +from neurom.check import morphology_checks from neurom.core.dataformat import COLS from neurom.core.types import dendrite_filter from neurom.exceptions import NeuroMError @@ -44,19 +44,18 @@ H5V1_PATH = DATA_PATH / 'h5/v1' - -def _load_neuron(name): +def _load_morphology(name): if name.endswith('.swc'): path = SWC_PATH / name elif name.endswith('.h5'): path = H5V1_PATH / name else: path = ASC_PATH / name - return name, load_neuron(path) + return name, load_morphology(path) -def _make_monotonic(neuron): - for neurite in neuron.neurites: +def _make_monotonic(morphology): + for neurite in morphology.neurites: for node in neurite.iter_sections(): sec = node.points if node.parent is not None: @@ -65,18 +64,18 @@ def _make_monotonic(neuron): sec[point_id + 1][COLS.R] = sec[point_id][COLS.R] / 2. -def _make_flat(neuron): +def _make_flat(morphology): class Flattenizer: def __call__(self, points): points = deepcopy(points) - points[:, COLS.Z] = 0.; + points[:, COLS.Z] = 0. return points - return neuron.transform(Flattenizer()) + return morphology.transform(Flattenizer()) -NEURONS = dict([_load_neuron(n) for n in ['Neuron.h5', +NEURONS = dict([_load_morphology(n) for n in ['Neuron.h5', 'Neuron_2_branch.h5', 'Neuron.swc', 'Neuron_small_radius.swc', @@ -99,36 +98,30 @@ def test_has_axon_good_data(): 'Single_axon.swc', 'Neuron.h5', ] - for n in _pick(files): - assert nrn_chk.has_axon(n) + for m in _pick(files): + assert morphology_checks.has_axon(m) def test_has_axon_bad_data(): - files = ['Single_apical.swc', - 'Single_basal.swc', - ] - for n in _pick(files): - assert not nrn_chk.has_axon(n) + files = ['Single_apical.swc', 'Single_basal.swc'] + for m in _pick(files): + assert not morphology_checks.has_axon(m) def test_has_apical_dendrite_good_data(): files = ['Neuron.swc', 'Neuron_small_radius.swc', 'Single_apical.swc', - 'Neuron.h5', - ] + 'Neuron.h5'] - for n in _pick(files): - assert nrn_chk.has_apical_dendrite(n) + for m in _pick(files): + assert morphology_checks.has_apical_dendrite(m) def test_has_apical_dendrite_bad_data(): - files = ['Single_axon.swc', - 'Single_basal.swc', - ] - - for n in _pick(files): - assert not nrn_chk.has_apical_dendrite(n) + files = ['Single_axon.swc', 'Single_basal.swc'] + for m in _pick(files): + assert not morphology_checks.has_apical_dendrite(m) def test_has_basal_dendrite_good_data(): @@ -136,33 +129,29 @@ def test_has_basal_dendrite_good_data(): 'Neuron_small_radius.swc', 'Single_basal.swc', 'Neuron_2_branch.h5', - 'Neuron.h5', - ] + 'Neuron.h5'] - for n in _pick(files): - assert nrn_chk.has_basal_dendrite(n) + for m in _pick(files): + assert morphology_checks.has_basal_dendrite(m) def test_has_basal_dendrite_bad_data(): - files = ['Single_axon.swc', - 'Single_apical.swc', - ] + files = ['Single_axon.swc', 'Single_apical.swc'] - for n in _pick(files): - assert not nrn_chk.has_basal_dendrite(n) + for m in _pick(files): + assert not morphology_checks.has_basal_dendrite(m) def test_has_no_flat_neurites(): + _, m = _load_morphology('Neuron.swc') - _, n = _load_neuron('Neuron.swc') - - assert nrn_chk.has_no_flat_neurites(n, 1e-6, method='tolerance') - assert nrn_chk.has_no_flat_neurites(n, 0.1, method='ratio') + assert morphology_checks.has_no_flat_neurites(m, 1e-6, method='tolerance') + assert morphology_checks.has_no_flat_neurites(m, 0.1, method='ratio') - n = _make_flat(n) + m = _make_flat(m) - assert not nrn_chk.has_no_flat_neurites(n, 1e-6, method='tolerance') - assert not nrn_chk.has_no_flat_neurites(n, 0.1, method='ratio') + assert not morphology_checks.has_no_flat_neurites(m, 1e-6, method='tolerance') + assert not morphology_checks.has_no_flat_neurites(m, 0.1, method='ratio') def test_nonzero_neurite_radii_good_data(): @@ -173,30 +162,30 @@ def test_nonzero_neurite_radii_good_data(): 'Neuron_2_branch.h5', ] - for n in _pick(files): - ids = nrn_chk.has_all_nonzero_neurite_radii(n) + for m in _pick(files): + ids = morphology_checks.has_all_nonzero_neurite_radii(m) assert len(ids.info) == 0 def test_has_all_nonzero_neurite_radii_threshold(): - nrn = NEURONS['Neuron.swc'] + m = NEURONS['Neuron.swc'] - ids = nrn_chk.has_all_nonzero_neurite_radii(nrn) + ids = morphology_checks.has_all_nonzero_neurite_radii(m) assert ids.status - ids = nrn_chk.has_all_nonzero_neurite_radii(nrn, threshold=0.25) + ids = morphology_checks.has_all_nonzero_neurite_radii(m, threshold=0.25) assert len(ids.info) == 122 def test_nonzero_neurite_radii_bad_data(): - nrn = NEURONS['Neuron_zero_radius.swc'] - ids = nrn_chk.has_all_nonzero_neurite_radii(nrn, threshold=0.7) + m = NEURONS['Neuron_zero_radius.swc'] + ids = morphology_checks.has_all_nonzero_neurite_radii(m, threshold=0.7) assert ids.info == [(0, 2)] def test_nonzero_segment_lengths_good_data(): - nrn = NEURONS['Neuron.swc'] - ids = nrn_chk.has_all_nonzero_segment_lengths(nrn) + m = NEURONS['Neuron.swc'] + ids = morphology_checks.has_all_nonzero_segment_lengths(m) assert ids.status assert len(ids.info) == 0 @@ -210,20 +199,20 @@ def test_nonzero_segment_lengths_bad_data(): bad_ids = [[0, 21, 42, 63], [0], [0], [0], [0]] - for i, nrn in enumerate(_pick(files)): - ids = nrn_chk.has_all_nonzero_segment_lengths(nrn) + for i, m in enumerate(_pick(files)): + ids = morphology_checks.has_all_nonzero_segment_lengths(m) assert (ids.info == [(id, 0) for id in bad_ids[i]]) def test_nonzero_segment_lengths_threshold(): - nrn = NEURONS['Neuron.swc'] + m = NEURONS['Neuron.swc'] - ids = nrn_chk.has_all_nonzero_segment_lengths(nrn) + ids = morphology_checks.has_all_nonzero_segment_lengths(m) assert ids.status assert len(ids.info) == 0 - ids = nrn_chk.has_all_nonzero_segment_lengths(nrn, threshold=0.25) + ids = morphology_checks.has_all_nonzero_segment_lengths(m, threshold=0.25) bad_ids = [(0, 0), (21, 0), (36, 9), (42, 0), (52, 7), (60, 2), (63, 0), (70, 4), (76, 6)] assert (ids.info == @@ -237,80 +226,79 @@ def test_nonzero_section_lengths_good_data(): 'Single_axon.swc', ] - for i, nrn in enumerate(_pick(files)): - ids = nrn_chk.has_all_nonzero_section_lengths(nrn) + for i, m in enumerate(_pick(files)): + ids = morphology_checks.has_all_nonzero_section_lengths(m) assert ids.status assert len(ids.info) == 0 def test_nonzero_section_lengths_bad_data(): - nrn = NEURONS['Neuron_zero_length_sections.swc'] + m = NEURONS['Neuron_zero_length_sections.swc'] - ids = nrn_chk.has_all_nonzero_section_lengths(nrn) + ids = morphology_checks.has_all_nonzero_section_lengths(m) assert not ids.status assert ids.info == [13] def test_nonzero_section_lengths_threshold(): - nrn = NEURONS['Neuron.swc'] + m = NEURONS['Neuron.swc'] - ids = nrn_chk.has_all_nonzero_section_lengths(nrn) + ids = morphology_checks.has_all_nonzero_section_lengths(m) assert ids.status assert len(ids.info) == 0 - ids = nrn_chk.has_all_nonzero_section_lengths(nrn, threshold=15.) + ids = morphology_checks.has_all_nonzero_section_lengths(m, threshold=15.) assert not ids.status assert len(ids.info) == 84 def test_has_nonzero_soma_radius(): - - nrn = load_neuron(SWC_PATH / 'Neuron.swc') - assert nrn_chk.has_nonzero_soma_radius(nrn) + m = load_morphology(SWC_PATH / 'Neuron.swc') + assert morphology_checks.has_nonzero_soma_radius(m) def test_has_nonzero_soma_radius_bad_data(): - nrn = load_neuron(SWC_PATH / 'soma_zero_radius.swc') - assert not nrn_chk.has_nonzero_soma_radius(nrn).status + m = load_morphology(SWC_PATH / 'soma_zero_radius.swc') + assert not morphology_checks.has_nonzero_soma_radius(m).status def test_has_no_fat_ends(): - _, nrn = _load_neuron('fat_end.swc') - assert not nrn_chk.has_no_fat_ends(nrn).status + _, m = _load_morphology('fat_end.swc') + assert not morphology_checks.has_no_fat_ends(m).status # if we only use point, there isn't a 'fat end' # since if the last point is 'x': x < 2*mean([x]) - assert nrn_chk.has_no_fat_ends(nrn, final_point_count=1).status + assert morphology_checks.has_no_fat_ends(m, final_point_count=1).status # if the multiple of the mean is large, the end won't be fat - assert nrn_chk.has_no_fat_ends(nrn, multiple_of_mean=10).status + assert morphology_checks.has_no_fat_ends(m, multiple_of_mean=10).status - _, nrn = _load_neuron('Single_basal.swc') - assert nrn_chk.has_no_fat_ends(nrn).status + _, m = _load_morphology('Single_basal.swc') + assert morphology_checks.has_no_fat_ends(m).status def test_has_no_root_node_jumps(): - _, nrn = _load_neuron('root_node_jump.swc') - check = nrn_chk.has_no_root_node_jumps(nrn) + _, m = _load_morphology('root_node_jump.swc') + check = morphology_checks.has_no_root_node_jumps(m) assert not check.status assert len(check.info) == 1 assert check.info[0][0] == 0 assert_array_equal(check.info[0][1], [[0, 3, 0]]) - assert nrn_chk.has_no_root_node_jumps(nrn, radius_multiplier=4).status + assert morphology_checks.has_no_root_node_jumps(m, radius_multiplier=4).status def test_has_no_narrow_start(): - _, nrn = _load_neuron('narrow_start.swc') - check = nrn_chk.has_no_narrow_start(nrn) + _, m = _load_morphology('narrow_start.swc') + check = morphology_checks.has_no_narrow_start(m) assert not check.status assert_array_equal(check.info[0][1][:, COLS.XYZR], [[0, 0, 2, 2]]) - _, nrn = _load_neuron('narrow_start.swc') - assert nrn_chk.has_no_narrow_start(nrn, 0.25).status + _, m = _load_morphology('narrow_start.swc') + assert morphology_checks.has_no_narrow_start(m, 0.25).status - _, nrn = _load_neuron('fat_end.swc') # doesn't have narrow start - assert nrn_chk.has_no_narrow_start(nrn, 0.25).status + _, m = _load_morphology('fat_end.swc') # doesn't have narrow start + assert morphology_checks.has_no_narrow_start(m, 0.25).status def test_has_nonzero_soma_radius_threshold(): @@ -318,26 +306,26 @@ def test_has_nonzero_soma_radius_threshold(): class Dummy: pass - nrn = Dummy() - nrn.soma = Dummy() - nrn.soma.radius = 1.5 + m = Dummy() + m.soma = Dummy() + m.soma.radius = 1.5 - assert nrn_chk.has_nonzero_soma_radius(nrn) - assert nrn_chk.has_nonzero_soma_radius(nrn, 0.25) - assert nrn_chk.has_nonzero_soma_radius(nrn, 0.75) - assert nrn_chk.has_nonzero_soma_radius(nrn, 1.25) - assert nrn_chk.has_nonzero_soma_radius(nrn, 1.499) - assert not nrn_chk.has_nonzero_soma_radius(nrn, 1.5) - assert not nrn_chk.has_nonzero_soma_radius(nrn, 1.75) - assert not nrn_chk.has_nonzero_soma_radius(nrn, 2.5) + assert morphology_checks.has_nonzero_soma_radius(m) + assert morphology_checks.has_nonzero_soma_radius(m, 0.25) + assert morphology_checks.has_nonzero_soma_radius(m, 0.75) + assert morphology_checks.has_nonzero_soma_radius(m, 1.25) + assert morphology_checks.has_nonzero_soma_radius(m, 1.499) + assert not morphology_checks.has_nonzero_soma_radius(m, 1.5) + assert not morphology_checks.has_nonzero_soma_radius(m, 1.75) + assert not morphology_checks.has_nonzero_soma_radius(m, 2.5) def test_has_no_jumps(): - _, nrn = _load_neuron('z_jump.swc') - assert not nrn_chk.has_no_jumps(nrn).status - assert nrn_chk.has_no_jumps(nrn, 100).status + _, m = _load_morphology('z_jump.swc') + assert not morphology_checks.has_no_jumps(m).status + assert morphology_checks.has_no_jumps(m, 100).status - assert nrn_chk.has_no_jumps(nrn, 100, axis='x').status + assert morphology_checks.has_no_jumps(m, 100, axis='x').status def test_has_no_narrow_dendritic_section(): @@ -353,15 +341,15 @@ def test_has_no_narrow_dendritic_section(): 8 3 6 -4 0 10. 7 9 3 -5 -4 0 10. 7 """) - nrn = load_neuron(swc_content, reader='swc') - res = nrn_chk.has_no_narrow_neurite_section(nrn, + m = load_morphology(swc_content, reader='swc') + res = morphology_checks.has_no_narrow_neurite_section(m, dendrite_filter, radius_threshold=5, considered_section_min_length=0) assert res.status - res = nrn_chk.has_no_narrow_neurite_section(nrn, dendrite_filter, + res = morphology_checks.has_no_narrow_neurite_section(m, dendrite_filter, radius_threshold=7, considered_section_min_length=0) assert not res.status @@ -378,37 +366,37 @@ def test_has_no_narrow_dendritic_section(): 8 3 6 -4 0 10. 7 9 3 -5 -4 0 10. 7 """) - nrn = load_neuron(swc_content, reader='swc') - res = nrn_chk.has_no_narrow_neurite_section(nrn, dendrite_filter, + m = load_morphology(swc_content, reader='swc') + res = morphology_checks.has_no_narrow_neurite_section(m, dendrite_filter, radius_threshold=5, considered_section_min_length=0) assert res.status, 'Narrow soma or axons should not raise bad status when checking for narrow dendrites' def test_has_no_dangling_branch(): - _, nrn = _load_neuron('dangling_axon.swc') - res = nrn_chk.has_no_dangling_branch(nrn) + _, m = _load_morphology('dangling_axon.swc') + res = morphology_checks.has_no_dangling_branch(m) assert not res.status assert len(res.info) == 1 assert_array_equal(res.info[0][1][0][COLS.XYZ], [0., 49., 0.]) - _, nrn = _load_neuron('dangling_dendrite.swc') - res = nrn_chk.has_no_dangling_branch(nrn) + _, m = _load_morphology('dangling_dendrite.swc') + res = morphology_checks.has_no_dangling_branch(m) assert not res.status assert len(res.info) == 1 assert_array_equal(res.info[0][1][0][COLS.XYZ], [0., 49., 0.]) - _, nrn = _load_neuron('axon-sprout-from-dendrite.asc') - res = nrn_chk.has_no_dangling_branch(nrn) + _, m = _load_morphology('axon-sprout-from-dendrite.asc') + res = morphology_checks.has_no_dangling_branch(m) assert res.status def test_dangling_branch_no_soma(): with pytest.raises(NeuroMError, match='Can\'t check for dangling neurites if there is no soma'): - nrn = load_neuron(SWC_PATH / 'Single_apical_no_soma.swc') - nrn_chk.has_no_dangling_branch(nrn) + m = load_morphology(SWC_PATH / 'Single_apical_no_soma.swc') + morphology_checks.has_no_dangling_branch(m) def test__bool__(): @@ -419,7 +407,7 @@ def test__bool__(): def test_has_multifurcation(): - nrn = load_neuron(StringIO(u""" + m = load_morphology(StringIO(u""" ((CellBody) (0 0 0 2)) ( (Color Blue) (Axon) @@ -442,7 +430,7 @@ def test_has_multifurcation(): ) """), reader='asc') - check_ = nrn_chk.has_multifurcation(nrn) + check_ = morphology_checks.has_multifurcation(m) assert not check_.status info = check_.info assert_array_equal(info[0][0], 0) @@ -450,7 +438,7 @@ def test_has_multifurcation(): def test_single_children(): - neuron = load_neuron(""" + m = load_morphology(""" ( (Color Blue) (Axon) (0 5 0 2) @@ -463,6 +451,6 @@ def test_single_children(): ) ) """, "asc") - result = nrn_chk.has_no_single_children(neuron) - assert result.status == False + result = morphology_checks.has_no_single_children(m) + assert result.status is False assert result.info == [0] diff --git a/tests/check/test_morphtree.py b/tests/check/test_morphtree.py index 2e1e4e862..c5c4a50ce 100644 --- a/tests/check/test_morphtree.py +++ b/tests/check/test_morphtree.py @@ -31,7 +31,7 @@ from pathlib import Path import numpy as np -from neurom import load_neuron +from neurom import load_morphology from neurom.check import morphtree as mt from neurom.core.dataformat import COLS @@ -39,7 +39,7 @@ SWC_PATH = DATA_PATH / 'swc' -def _make_flat(neuron): +def _make_flat(morphology): class Flattenizer: def __call__(self, points): @@ -47,11 +47,11 @@ def __call__(self, points): points[:, COLS.Z] = 0. return points - return neuron.transform(Flattenizer()) + return morphology.transform(Flattenizer()) -def _make_monotonic(neuron): - for neurite in neuron.neurites: +def _make_monotonic(morphology): + for neurite in morphology.neurites: for section in neurite.iter_sections(): points = section.points if section.parent is not None: @@ -64,7 +64,7 @@ def _make_monotonic(neuron): def _generate_back_track_tree(n, dev): points = np.array(dev) + np.array([1, 3 if n == 0 else -3, 0]) - neuron = load_neuron(StringIO(u""" + m = load_morphology(StringIO(u""" ((CellBody) (0 0 0 0.4)) @@ -86,12 +86,12 @@ def _generate_back_track_tree(n, dev): )) """.format(*points.tolist())), reader='asc') - return neuron + return m def test_is_monotonic(): # tree with decreasing radii - neuron = load_neuron(StringIO(u""" + m = load_morphology(StringIO(u""" ((Dendrite) (0 0 0 1.0) (0 0 0 0.99) @@ -102,10 +102,10 @@ def test_is_monotonic(): (0 0 0 0.5) (0 0 0 0.2) ))"""), reader='asc') - assert mt.is_monotonic(neuron.neurites[0], 1e-6) + assert mt.is_monotonic(m.neurites[0], 1e-6) # tree with equal radii - neuron = load_neuron(StringIO(u""" + m = load_morphology(StringIO(u""" ((Dendrite) (0 0 0 1.0) (0 0 0 1.0) @@ -116,10 +116,10 @@ def test_is_monotonic(): (0 0 0 1.0) (0 0 0 1.0) ))"""), reader='asc') - assert mt.is_monotonic(neuron.neurites[0], 1e-6) + assert mt.is_monotonic(m.neurites[0], 1e-6) # tree with increasing radii - neuron = load_neuron(StringIO(u""" + m = load_morphology(StringIO(u""" ((Dendrite) (0 0 0 1.0) (0 0 0 1.0) @@ -130,10 +130,10 @@ def test_is_monotonic(): (0 0 0 0.3) (0 0 0 0.1) ))"""), reader='asc') - assert not mt.is_monotonic(neuron.neurites[0], 1e-6) + assert not mt.is_monotonic(m.neurites[0], 1e-6) # Tree with larger child initial point - neuron = load_neuron(StringIO(u""" + m = load_morphology(StringIO(u""" ((Dendrite) (0 0 0 1.0) (0 0 0 0.75) @@ -144,13 +144,13 @@ def test_is_monotonic(): (0 0 0 0.125) (0 0 0 0.625) ))"""), reader='asc') - assert not mt.is_monotonic(neuron.neurites[0], 1e-6) + assert not mt.is_monotonic(m.neurites[0], 1e-6) def test_is_flat(): - neu_tree = load_neuron(Path(SWC_PATH, 'Neuron.swc')) - assert not mt.is_flat(neu_tree.neurites[0], 1e-6, method='tolerance') - assert not mt.is_flat(neu_tree.neurites[0], 0.1, method='ratio') + m = load_morphology(Path(SWC_PATH, 'Neuron.swc')) + assert not mt.is_flat(m.neurites[0], 1e-6, method='tolerance') + assert not mt.is_flat(m.neurites[0], 0.1, method='ratio') def test_is_back_tracking(): @@ -178,22 +178,22 @@ def test_is_back_tracking(): def test_get_flat_neurites(): - n = load_neuron(Path(SWC_PATH, 'Neuron.swc')) - assert len(mt.get_flat_neurites(n, 1e-6, method='tolerance')) == 0 - assert len(mt.get_flat_neurites(n, 0.1, method='ratio')) == 0 + m = load_morphology(Path(SWC_PATH, 'Neuron.swc')) + assert len(mt.get_flat_neurites(m, 1e-6, method='tolerance')) == 0 + assert len(mt.get_flat_neurites(m, 0.1, method='ratio')) == 0 - n = _make_flat(n) - assert len(mt.get_flat_neurites(n, 1e-6, method='tolerance')) == 4 - assert len(mt.get_flat_neurites(n, 0.1, method='ratio')) == 4 + m = _make_flat(m) + assert len(mt.get_flat_neurites(m, 1e-6, method='tolerance')) == 4 + assert len(mt.get_flat_neurites(m, 0.1, method='ratio')) == 4 def test_get_nonmonotonic_neurites(): - n = load_neuron(Path(SWC_PATH, 'Neuron.swc')) - assert len(mt.get_nonmonotonic_neurites(n)) == 4 - _make_monotonic(n) - assert len(mt.get_nonmonotonic_neurites(n)) == 0 + m = load_morphology(Path(SWC_PATH, 'Neuron.swc')) + assert len(mt.get_nonmonotonic_neurites(m)) == 4 + _make_monotonic(m) + assert len(mt.get_nonmonotonic_neurites(m)) == 0 def test_get_back_tracking_neurites(): - n = load_neuron(Path(SWC_PATH, 'Neuron.swc')) - assert len(mt.get_back_tracking_neurites(n)) == 4 + m = load_morphology(Path(SWC_PATH, 'Neuron.swc')) + assert len(mt.get_back_tracking_neurites(m)) == 4 diff --git a/tests/check/test_runner.py b/tests/check/test_runner.py index 92a55c5e5..6c7e8336e 100644 --- a/tests/check/test_runner.py +++ b/tests/check/test_runner.py @@ -37,7 +37,7 @@ SWC_PATH = Path(__file__).parent.parent / 'data/swc/' CONFIG = { 'checks': { - 'neuron_checks': [ + 'morphology_checks': [ 'has_basal_dendrite', 'has_axon', 'has_apical_dendrite', @@ -77,19 +77,19 @@ def _run_test(path, ref, config=CONFIG, should_pass=False): ("ALL", True) ]) -def test_ok_neuron(): +def test_ok_morphology(): _run_test(SWC_PATH / 'Neuron.swc', ref, should_pass=True) -def test_ok_neuron_color(): +def test_ok_morphology_color(): _run_test(SWC_PATH / 'Neuron.swc', ref, CONFIG_COLOR, should_pass=True) -def test_zero_length_sections_neuron(): +def test_zero_length_sections_morphology(): expected = dict([ ("Has basal dendrite", True), ("Has axon", True), @@ -104,7 +104,7 @@ def test_zero_length_sections_neuron(): expected) -def test_single_apical_neuron(): +def test_single_apical_morphology(): expected = dict([ ("Has basal dendrite", False), ("Has axon", False), @@ -119,7 +119,7 @@ def test_single_apical_neuron(): expected) -def test_single_basal_neuron(): +def test_single_basal_morphology(): expected = dict( ([ ("Has basal dendrite", True), @@ -135,7 +135,7 @@ def test_single_basal_neuron(): expected) -def test_single_axon_neuron(): +def test_single_axon_morphology(): expected = dict([ ("Has basal dendrite", False), ("Has axon", True), @@ -184,7 +184,7 @@ def test__sanitize_config(): new_config = CheckRunner._sanitize_config({'checks': {}}) assert new_config == {'checks': { - 'neuron_checks': [], + 'morphology_checks': [], }, 'options': {}, 'color': False, diff --git a/tests/check/test_structural_checks.py b/tests/check/test_structural_checks.py deleted file mode 100644 index 46d57a489..000000000 --- a/tests/check/test_structural_checks.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import pytest - - -def test_import_tree(): - with pytest.raises(NotImplementedError): - import neurom.check.structural_checks diff --git a/tests/core/test_iter.py b/tests/core/test_iter.py index 5febab765..01f3bc405 100644 --- a/tests/core/test_iter.py +++ b/tests/core/test_iter.py @@ -25,28 +25,28 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - +import warnings from io import StringIO from pathlib import Path import neurom as nm -from neurom import COLS, load_neuron -from neurom.core.neuron import NeuriteIter, Section, iter_neurites, iter_sections, iter_segments +from neurom import COLS, load_morphology +from neurom.core.morphology import NeuriteIter, Section, iter_neurites, iter_sections, iter_segments from neurom.core.population import Population from numpy.testing import assert_array_equal DATA_PATH = Path(__file__).parent.parent / 'data' -NRN1 = load_neuron(DATA_PATH / 'swc/Neuron.swc') +NRN1 = load_morphology(DATA_PATH / 'swc/Neuron.swc') NEURONS = [NRN1, - load_neuron(DATA_PATH / 'swc/Single_basal.swc'), - load_neuron(DATA_PATH / 'swc/Neuron_small_radius.swc'), - load_neuron(DATA_PATH / 'swc/Neuron_3_random_walker_branches.swc'),] + load_morphology(DATA_PATH / 'swc/Single_basal.swc'), + load_morphology(DATA_PATH / 'swc/Neuron_small_radius.swc'), + load_morphology(DATA_PATH / 'swc/Neuron_3_random_walker_branches.swc'), ] TOT_NEURITES = sum(len(N.neurites) for N in NEURONS) -SIMPLE = load_neuron(DATA_PATH / 'swc/simple.swc') -REVERSED_NEURITES = load_neuron(DATA_PATH / 'swc/ordering/reversed_NRN_neurite_order.swc') +SIMPLE = load_morphology(DATA_PATH / 'swc/simple.swc') +REVERSED_NEURITES = load_morphology(DATA_PATH / 'swc/ordering/reversed_NRN_neurite_order.swc') POP = Population(NEURONS, name='foo') @@ -89,6 +89,14 @@ def test_iter_neurites_filter_mapping(): assert n == ref +def test_iter_population(): + with warnings.catch_warnings(record=True) as w_list: + for _ in iter_neurites(POP, neurite_order=NeuriteIter.NRN): + pass + assert len(w_list) == 1 + assert '`iter_neurites` with `neurite_order` over Population' in str(w_list[0].message) + + def test_iter_sections_default(): ref = [s for n in POP.neurites for s in n.iter_sections()] @@ -135,7 +143,7 @@ def test_iter_sections_ileaf(): [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 83, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 20, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 83, 0, 1, 2]) -def test_iter_section_nrn(): +def test_iter_section_morph(): ref = list(iter_sections(SIMPLE)) assert len(ref) == 6 @@ -149,7 +157,7 @@ def test_iter_section_nrn(): assert len(ref) == 0 -def test_iter_segments_nrn(): +def test_iter_segments_morph(): ref = list(iter_segments(SIMPLE)) assert len(ref) == 6 @@ -191,7 +199,7 @@ def test_iter_segments_pop(): def test_iter_segments_section(): - sec = load_neuron(StringIO(u""" + sec = load_morphology(StringIO(u""" ((CellBody) (0 0 0 2)) diff --git a/tests/core/test_neurite.py b/tests/core/test_neurite.py index 1807d33da..234530daf 100644 --- a/tests/core/test_neurite.py +++ b/tests/core/test_neurite.py @@ -30,14 +30,14 @@ from pathlib import Path import neurom as nm -from neurom.core.neuron import Neurite +from neurom.core.morphology import Neurite from numpy.testing import assert_almost_equal SWC_PATH = Path(__file__).parent.parent / 'data/swc/' -nrn = nm.load_neuron(SWC_PATH / 'point_soma_single_neurite.swc') +m = nm.load_morphology(SWC_PATH / 'point_soma_single_neurite.swc') -ROOT_NODE = nrn.neurites[0].morphio_root_node +ROOT_NODE = m.neurites[0].morphio_root_node RADIUS = .5 REF_LEN = 3 diff --git a/tests/core/test_neuron.py b/tests/core/test_neuron.py index 44dd50ddd..4e1fbf73f 100644 --- a/tests/core/test_neuron.py +++ b/tests/core/test_neuron.py @@ -31,23 +31,22 @@ import neurom as nm import numpy as np -from morphio import Morphology as ImmutMorphology -from morphio.mut import Morphology -from neurom.core.neuron import Neuron, graft_neuron, iter_segments +import morphio +from neurom.core.morphology import Morphology, graft_morphology, iter_segments from numpy.testing import assert_array_equal SWC_PATH = Path(__file__).parent.parent / 'data/swc/' def test_simple(): - nm.load_neuron(str(SWC_PATH / 'simple.swc')) + nm.load_morphology(str(SWC_PATH / 'simple.swc')) -def test_load_neuron_pathlib(): - nm.load_neuron(SWC_PATH / 'simple.swc') +def test_load_morphology_pathlib(): + nm.load_morphology(SWC_PATH / 'simple.swc') -def test_load_neuron_from_other_neurons(): +def test_load_morphology_from_other_morphologies(): filename = SWC_PATH / 'simple.swc' expected_points = [[ 0., 0., 0., 1.], @@ -63,84 +62,84 @@ def test_load_neuron_from_other_neurons(): [ 0., -4., 0., 1.], [-5., -4., 0., 0.]] - assert_array_equal(nm.load_neuron(nm.load_neuron(filename)).points, + assert_array_equal(nm.load_morphology(nm.load_morphology(filename)).points, expected_points) - assert_array_equal(nm.load_neuron(Morphology(filename)).points, + assert_array_equal(nm.load_morphology(Morphology(filename)).points, expected_points) - assert_array_equal(nm.load_neuron(ImmutMorphology(filename)).points, + assert_array_equal(nm.load_morphology(morphio.Morphology(filename)).points, expected_points) def test_for_morphio(): - Neuron(Morphology()) + Morphology(morphio.mut.Morphology()) - neuron = Morphology() - neuron.soma.points = [[0,0,0], [1,1,1], [2,2,2]] - neuron.soma.diameters = [1, 1, 1] + morphio_m = morphio.mut.Morphology() + morphio_m.soma.points = [[0,0,0], [1,1,1], [2,2,2]] + morphio_m.soma.diameters = [1, 1, 1] - NeuroM_neuron = Neuron(neuron) - assert_array_equal(NeuroM_neuron.soma.points, + neurom_m = Morphology(morphio_m) + assert_array_equal(neurom_m.soma.points, [[0., 0., 0., 0.5], [1., 1., 1., 0.5], [2., 2., 2., 0.5]]) - NeuroM_neuron.soma.points = [[1, 1, 1, 1], + neurom_m.soma.points = [[1, 1, 1, 1], [2, 2, 2, 2]] - assert_array_equal(NeuroM_neuron.soma.points, + assert_array_equal(neurom_m.soma.points, [[1, 1, 1, 1], [2, 2, 2, 2]]) -def _check_cloned_neuron(nrn1, nrn2): - # check if two neurons are identical +def _check_cloned_morphology(m, m2): + # check if two morphs are identical # soma - assert isinstance(nrn2.soma, type(nrn1.soma)) - assert nrn1.soma.radius == nrn2.soma.radius + assert isinstance(m2.soma, type(m.soma)) + assert m.soma.radius == m2.soma.radius - for v1, v2 in zip(nrn1.soma.iter(), nrn2.soma.iter()): + for v1, v2 in zip(m.soma.iter(), m2.soma.iter()): assert np.allclose(v1, v2) # neurites - for v1, v2 in zip(iter_segments(nrn1), iter_segments(nrn2)): + for v1, v2 in zip(iter_segments(m), iter_segments(m2)): (v1_start, v1_end), (v2_start, v2_end) = v1, v2 assert np.allclose(v1_start, v2_start) assert np.allclose(v1_end, v2_end) # check if the ids are different # somata - assert nrn1.soma is not nrn2.soma + assert m.soma is not m2.soma # neurites - for neu1, neu2 in zip(nrn1.neurites, nrn2.neurites): + for neu1, neu2 in zip(m.neurites, m2.neurites): assert neu1 is not neu2 - # check if changes are propagated between neurons - nrn2.soma.radius = 10. - assert nrn1.soma.radius != nrn2.soma.radius + # check if changes are propagated between morphs + m2.soma.radius = 10. + assert m.soma.radius != m2.soma.radius def test_copy(): - nrn = nm.load_neuron(SWC_PATH / 'simple.swc') - _check_cloned_neuron(nrn, copy(nrn)) + m = nm.load_morphology(SWC_PATH / 'simple.swc') + _check_cloned_morphology(m, copy(m)) def test_deepcopy(): - nrn = nm.load_neuron(SWC_PATH / 'simple.swc') - _check_cloned_neuron(nrn, deepcopy(nrn)) + m = nm.load_morphology(SWC_PATH / 'simple.swc') + _check_cloned_morphology(m, deepcopy(m)) -def test_graft_neuron(): - nrn1 = nm.load_neuron(SWC_PATH / 'simple.swc') - basal_dendrite = nrn1.neurites[0] - nrn2 = graft_neuron(basal_dendrite.root_node) - assert len(nrn2.neurites) == 1 - assert basal_dendrite == nrn2.neurites[0] +def test_graft_morphology(): + m = nm.load_morphology(SWC_PATH / 'simple.swc') + basal_dendrite = m.neurites[0] + m2 = graft_morphology(basal_dendrite.root_node) + assert len(m2.neurites) == 1 + assert basal_dendrite == m2.neurites[0] def test_str(): - n = nm.load_neuron(SWC_PATH / 'simple.swc') - assert 'Neuron' in str(n) + n = nm.load_morphology(SWC_PATH / 'simple.swc') + assert 'Morphology' in str(n) assert 'Section' in str(n.neurites[0].root_node) diff --git a/tests/core/test_population.py b/tests/core/test_population.py index 8c07f1952..e52cf298d 100644 --- a/tests/core/test_population.py +++ b/tests/core/test_population.py @@ -29,8 +29,8 @@ from pathlib import Path from neurom.core.population import Population -from neurom.core.neuron import Neuron -from neurom import load_neuron +from neurom.core.morphology import Morphology +from neurom import load_morphology import pytest @@ -40,7 +40,7 @@ DATA_PATH / 'swc/Single_basal.swc', DATA_PATH / 'swc/Neuron_small_radius.swc'] -NEURONS = [load_neuron(f) for f in FILES] +NEURONS = [load_morphology(f) for f in FILES] TOT_NEURITES = sum(len(N.neurites) for N in NEURONS) populations = [Population(NEURONS, name='foo'), Population(FILES, name='foo', cache=True)] @@ -48,7 +48,7 @@ @pytest.mark.parametrize('pop', populations) def test_names(pop): - assert pop[0].name, 'Neuron' + assert pop[0].name, 'Morphology' assert pop[1].name, 'Single_basal' assert pop[2].name, 'Neuron_small_radius' assert pop.name == 'foo' @@ -65,7 +65,7 @@ def test_indexing(): def test_cache(): pop = populations[1] for n in pop._files: - assert isinstance(n, Neuron) + assert isinstance(n, Morphology) def test_double_indexing(): diff --git a/tests/core/test_section.py b/tests/core/test_section.py index c0c9ba30c..25fc48179 100644 --- a/tests/core/test_section.py +++ b/tests/core/test_section.py @@ -35,8 +35,8 @@ def test_section_base_func(): - nrn = nm.load_neuron(str(SWC_PATH / 'simple.swc')) - section = nrn.sections[0] + m = nm.load_morphology(str(SWC_PATH / 'simple.swc')) + section = m.sections[0] assert section.type == nm.NeuriteType.basal_dendrite assert section.id == 0 @@ -47,39 +47,39 @@ def test_section_base_func(): def test_section_tree(): - nrn = nm.load_neuron(str(SWC_PATH / 'simple.swc')) + m = nm.load_morphology(str(SWC_PATH / 'simple.swc')) - assert nrn.sections[0].parent is None - assert nrn.sections[0] == nrn.sections[0].children[0].parent + assert m.sections[0].parent is None + assert m.sections[0] == m.sections[0].children[0].parent - assert_array_equal([s.is_root() for s in nrn.sections], + assert_array_equal([s.is_root() for s in m.sections], [True, False, False, True, False, False]) - assert_array_equal([s.is_leaf() for s in nrn.sections], + assert_array_equal([s.is_leaf() for s in m.sections], [False, True, True, False, True, True]) - assert_array_equal([s.is_forking_point() for s in nrn.sections], + assert_array_equal([s.is_forking_point() for s in m.sections], [True, False, False, True, False, False]) - assert_array_equal([s.is_bifurcation_point() for s in nrn.sections], + assert_array_equal([s.is_bifurcation_point() for s in m.sections], [True, False, False, True, False, False]) - assert_array_equal([s.id for s in nrn.neurites[0].root_node.ipreorder()], + assert_array_equal([s.id for s in m.neurites[0].root_node.ipreorder()], [0, 1, 2]) - assert_array_equal([s.id for s in nrn.neurites[0].root_node.ipostorder()], + assert_array_equal([s.id for s in m.neurites[0].root_node.ipostorder()], [1, 2, 0]) - assert_array_equal([s.id for s in nrn.neurites[0].root_node.iupstream()], + assert_array_equal([s.id for s in m.neurites[0].root_node.iupstream()], [0]) - assert_array_equal([s.id for s in nrn.sections[2].iupstream()], + assert_array_equal([s.id for s in m.sections[2].iupstream()], [2, 0]) - assert_array_equal([s.id for s in nrn.neurites[0].root_node.ileaf()], + assert_array_equal([s.id for s in m.neurites[0].root_node.ileaf()], [1, 2]) - assert_array_equal([s.id for s in nrn.sections[2].ileaf()], + assert_array_equal([s.id for s in m.sections[2].ileaf()], [2]) - assert_array_equal([s.id for s in nrn.neurites[0].root_node.iforking_point()], + assert_array_equal([s.id for s in m.neurites[0].root_node.iforking_point()], [0]) - assert_array_equal([s.id for s in nrn.neurites[0].root_node.ibifurcation_point()], + assert_array_equal([s.id for s in m.neurites[0].root_node.ibifurcation_point()], [0]) def test_append_section(): - n = nm.load_neuron(SWC_PATH / 'simple.swc') + n = nm.load_morphology(SWC_PATH / 'simple.swc') s = n.sections[0] s.append_section(n.sections[-1]) @@ -94,7 +94,7 @@ def test_append_section(): def test_set_points(): - n = nm.load_neuron(SWC_PATH / 'simple.swc') + n = nm.load_morphology(SWC_PATH / 'simple.swc') s = n.sections[0] s.points = np.array([ [0, 5, 0, 2], diff --git a/tests/core/test_soma.py b/tests/core/test_soma.py index 769ee6662..517aeba03 100644 --- a/tests/core/test_soma.py +++ b/tests/core/test_soma.py @@ -32,7 +32,7 @@ import numpy as np from morphio import MorphioError, SomaError, set_raise_warnings -from neurom import load_neuron +from neurom import load_morphology from neurom.core import soma from mock import Mock @@ -48,7 +48,7 @@ def test_no_soma_builder(): def test_no_soma(): - sm = load_neuron(StringIO(u""" + sm = load_morphology(StringIO(u""" ((Dendrite) (0 0 0 1.0) (0 0 0 2.0))"""), reader='asc').soma @@ -57,7 +57,7 @@ def test_no_soma(): def test_Soma_SinglePoint(): - sm = load_neuron(StringIO(u"""1 1 11 22 33 44 -1"""), reader='swc').soma + sm = load_morphology(StringIO(u"""1 1 11 22 33 44 -1"""), reader='swc').soma assert 'SomaSinglePoint' in str(sm) assert isinstance(sm, soma.SomaSinglePoint) assert list(sm.center) == [11, 22, 33] @@ -66,7 +66,7 @@ def test_Soma_SinglePoint(): def test_Soma_contour(): with warnings.catch_warnings(record=True): - sm = load_neuron(StringIO(u"""((CellBody) + sm = load_morphology(StringIO(u"""((CellBody) (0 0 0 44) (0 -44 0 44) (0 +44 0 44))"""), reader='asc').soma @@ -78,7 +78,7 @@ def test_Soma_contour(): def test_Soma_ThreePointCylinder(): - sm = load_neuron(StringIO(u"""1 1 0 0 0 44 -1 + sm = load_morphology(StringIO(u"""1 1 0 0 0 44 -1 2 1 0 -44 0 44 1 3 1 0 +44 0 44 1"""), reader='swc').soma assert 'SomaNeuromorphoThreePointCylinders' in str(sm) @@ -89,7 +89,7 @@ def test_Soma_ThreePointCylinder(): def test_Soma_ThreePointCylinder_invalid_radius(): with warnings.catch_warnings(record=True) as w_list: - load_neuron(StringIO(u""" + load_morphology(StringIO(u""" 1 1 0 0 0 1e-8 -1 2 1 0 -1e-8 0 1e-8 1 3 1 0 +1e-8 0 1e-8 1"""), reader='swc').soma @@ -101,7 +101,7 @@ def test_Soma_ThreePointCylinder_invalid(): set_raise_warnings(True) with pytest.raises(MorphioError, match='Warning: the soma does not conform the three point soma spec'): - load_neuron(StringIO(u""" + load_morphology(StringIO(u""" 1 1 0 0 0 1e-4 -1 2 1 0 -44 0 1e-4 1 3 1 0 +44 0 1e-4 1"""), reader='swc') @@ -110,7 +110,7 @@ def test_Soma_ThreePointCylinder_invalid(): def check_SomaC(stream): - sm = load_neuron(StringIO(stream), reader='asc').soma + sm = load_morphology(StringIO(stream), reader='asc').soma assert 'SomaSimpleContour' in str(sm) assert isinstance(sm, soma.SomaSimpleContour) np.testing.assert_almost_equal(sm.center, [0., 0., 0.]) @@ -150,16 +150,16 @@ def test_SomaC(): def test_soma_points_2(): - load_neuron(StringIO(u""" + load_morphology(StringIO(u""" 1 1 0 0 -10 40 -1 2 1 0 0 0 40 1"""), reader='swc').soma - load_neuron(StringIO(u"""((CellBody) + load_morphology(StringIO(u"""((CellBody) (0 0 0 44) (0 +44 0 44))"""), reader='asc').soma def test_Soma_Cylinders(): - s = load_neuron(StringIO(u""" + s = load_morphology(StringIO(u""" 1 1 0 0 -10 40 -1 2 1 0 0 0 40 1 3 1 0 0 10 40 2"""), reader='swc').soma @@ -174,7 +174,7 @@ def test_Soma_Cylinders(): # neuromorpho style with warnings.catch_warnings(record=True): - s = load_neuron(StringIO(u""" + s = load_morphology(StringIO(u""" 1 1 0 0 0 10 -1 2 1 0 -10 0 10 1 3 1 0 10 0 10 1"""), reader='swc').soma @@ -187,7 +187,7 @@ def test_Soma_Cylinders(): #but have (ys + rs) as point 2, and have xs different in each line # ex: http://neuromorpho.org/dableFiles/brumberg/CNG%20version/april11s1cell-1.CNG.swc with warnings.catch_warnings(record=True): - s = load_neuron(StringIO(u""" + s = load_morphology(StringIO(u""" 1 1 0 0 0 10 -1 2 1 -2 -6 0 10 1 3 1 2 6 0 10 1"""), reader='swc').soma @@ -196,7 +196,7 @@ def test_Soma_Cylinders(): assert list(s.center) == [0., 0., 0.] assert_almost_equal(s.area, 794.76706126368811, decimal=5) - s = load_neuron(StringIO(u""" + s = load_morphology(StringIO(u""" 1 1 0 0 0 0 -1 2 1 0 2 0 2 1 3 1 0 4 0 4 2 diff --git a/tests/core/test_tree.py b/tests/core/test_tree.py deleted file mode 100644 index fe7c215ed..000000000 --- a/tests/core/test_tree.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import pytest - - -def test_import_tree(): - with pytest.raises(NotImplementedError): - import neurom.core.tree diff --git a/tests/data/extracted-stats.csv b/tests/data/extracted-stats.csv index ab6f2a40d..d02d894bc 100644 --- a/tests/data/extracted-stats.csv +++ b/tests/data/extracted-stats.csv @@ -1,4 +1,4 @@ -,property,axon,axon,axon,axon,axon,axon,axon,axon,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,all,all,all,all,all,all,all,all,neuron,neuron -,name,max_section_length,total_section_length,total_section_volume,max_section_branch_order,max_segment_midpoint_0,max_segment_midpoint_1,max_segment_midpoint_2,mean_max_radial_distance,max_section_length,total_section_length,total_section_volume,max_section_branch_order,max_segment_midpoint_0,max_segment_midpoint_1,max_segment_midpoint_2,mean_max_radial_distance,max_section_length,total_section_length,total_section_volume,max_section_branch_order,max_segment_midpoint_0,max_segment_midpoint_1,max_segment_midpoint_2,mean_max_radial_distance,max_section_length,total_section_length,total_section_volume,max_section_branch_order,max_segment_midpoint_0,max_segment_midpoint_1,max_segment_midpoint_2,mean_max_radial_distance,mean_soma_radius,mean_max_radial_distance +,property,axon,axon,axon,axon,axon,axon,axon,axon,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,apical_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,basal_dendrite,all,all,all,all,all,all,all,all,morphology,morphology +,name,max_section_lengths,sum_section_lengths,sum_section_volumes,max_section_branch_orders,max_segment_midpoints_0,max_segment_midpoints_1,max_segment_midpoints_2,mean_max_radial_distance,max_section_lengths,sum_section_lengths,sum_section_volumes,max_section_branch_orders,max_segment_midpoints_0,max_segment_midpoints_1,max_segment_midpoints_2,mean_max_radial_distance,max_section_lengths,sum_section_lengths,sum_section_volumes,max_section_branch_orders,max_segment_midpoints_0,max_segment_midpoints_1,max_segment_midpoints_2,mean_max_radial_distance,max_section_lengths,sum_section_lengths,sum_section_volumes,max_section_branch_orders,max_segment_midpoints_0,max_segment_midpoints_1,max_segment_midpoints_2,mean_max_radial_distance,mean_soma_radius,mean_max_radial_distance 0,Neuron.swc,11.018460736176685,207.8797522090813,276.7385765728952,10,0.0,0.0,49.52030596415,82.44254511788921,11.758281556059444,214.37304577550353,271.9412385728449,10.0,64.40167498405,0.0,53.750947521650005,99.5894610648815,11.652508126101711,418.43241643793476,556.2279268208382,10,64.00787233325,48.48197694465,51.575580778049996,94.43342438865741,11.758281556059444,840.6852144225195,1104.9077419665782,10,64.40167498405,48.48197694465,53.750947521650005,99.5894610648815,0.13065629648763766,99.5894610648815 1,simple.swc,6.0,15.0,24.08554367752175,1,3.0,-2.0,0.0,7.211102550927978,,0.0,0.0,,,,,0.0,6.0,16.0,27.227136331111538,1,3.0,5.0,0.0,7.810249675906654,6.0,31.0,51.312680008633286,1,3.0,5.0,0.0,7.810249675906654,1.0,7.810249675906654 diff --git a/tests/features/__init__.py b/tests/features/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/features/test_bifurcationfunc.py b/tests/features/test_bifurcation.py similarity index 93% rename from tests/features/test_bifurcationfunc.py rename to tests/features/test_bifurcation.py index c67e0c2ae..530a4c4e7 100644 --- a/tests/features/test_bifurcationfunc.py +++ b/tests/features/test_bifurcation.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Test neurom._bifurcationfunc functionality.""" +"""Test features.bifurcationfunc.""" from pathlib import Path import warnings @@ -35,18 +35,18 @@ from numpy.testing import assert_raises import neurom as nm -from neurom import load_neuron +from neurom import load_morphology from neurom.exceptions import NeuroMError -from neurom.features import bifurcationfunc as bf +from neurom.features import bifurcation as bf import pytest DATA_PATH = Path(__file__).parent.parent / 'data' SWC_PATH = DATA_PATH / 'swc' -SIMPLE = nm.load_neuron(SWC_PATH / 'simple.swc') +SIMPLE = nm.load_morphology(SWC_PATH / 'simple.swc') with warnings.catch_warnings(record=True): - SIMPLE2 = load_neuron(DATA_PATH / 'neurolucida' / 'not_too_complex.asc') - MULTIFURCATION = load_neuron(DATA_PATH / 'neurolucida' / 'multifurcation.asc') + SIMPLE2 = load_morphology(DATA_PATH / 'neurolucida' / 'not_too_complex.asc') + MULTIFURCATION = load_morphology(DATA_PATH / 'neurolucida' / 'multifurcation.asc') def test_local_bifurcation_angle(): diff --git a/tests/features/test_feature_compat.py b/tests/features/test_feature_compat.py deleted file mode 100644 index 8fbf0f4bb..000000000 --- a/tests/features/test_feature_compat.py +++ /dev/null @@ -1,220 +0,0 @@ -# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""compare neurom.features features with values dumped from the original -neurom._point_neurite.features""" - -import json -import warnings -from itertools import chain -from pathlib import Path - -import neurom as nm -from neurom.core.neuron import Section -from neurom.core.types import NeuriteType -from neurom.features import bifurcationfunc as _bf -from neurom.features import neuritefunc as _nrt -from neurom.features import neuronfunc as _nrn -from neurom.features import sectionfunc as _sec - -from numpy.testing import assert_almost_equal -from utils import _close, _equal - -DATA_PATH = Path(__file__).parent.parent / 'data' -SWC_DATA_PATH = DATA_PATH / 'swc' -H5V1_DATA_PATH = DATA_PATH / 'h5/v1' -H5V2_DATA_PATH = DATA_PATH / 'h5/v2' -MORPH_FILENAME = 'Neuron.h5' -SWC_MORPH_FILENAME = 'Neuron.swc' - -REF_NEURITE_TYPES = [NeuriteType.apical_dendrite, NeuriteType.basal_dendrite, - NeuriteType.basal_dendrite, NeuriteType.axon] - -json_data = json.load(open(DATA_PATH / 'dataset/point_neuron_feature_values.json')) - - -def get(feat, neurite_format, **kwargs): - """using the values captured from the old point_neurite system.""" - neurite_type = str(kwargs.get('neurite_type', '')) - return json_data[neurite_format][feat][neurite_type] - - -def i_chain2(trees, iterator_type=Section.ipreorder, mapping=None, tree_filter=None): - """Returns a mapped iterator to a collection of trees - Provides access to all the elements of all the trees - in one iteration sequence. - Parameters: - trees: iterator or iterable of tree objects - iterator_type: type of the iteration (segment, section, triplet...) - mapping: optional function to apply to the iterator's target. - tree_filter: optional top level filter on properties of tree objects. - """ - nrt = (trees if tree_filter is None - else filter(tree_filter, trees)) - - chain_it = chain.from_iterable(map(iterator_type, nrt)) - return chain_it if mapping is None else map(mapping, chain_it) - - -class SectionTreeBase: - """Base class for section tree tests.""" - - def setup_method(self): - self.ref_nrn = 'h5' - self.ref_types = REF_NEURITE_TYPES - - def test_neurite_type(self): - neurite_types = [n0.type for n0 in self.sec_nrn.neurites] - assert neurite_types == self.ref_types - - def test_get_n_sections(self): - assert (_nrt.n_sections(self.sec_nrn) == - get('number_of_sections', self.ref_nrn)[0]) - - for t in NeuriteType: - actual = _nrt.n_sections(self.sec_nrn, neurite_type=t) - assert (actual == - get('number_of_sections', self.ref_nrn, neurite_type=t)[0]) - - def test_get_number_of_sections_per_neurite(self): - _equal(_nrt.number_of_sections_per_neurite(self.sec_nrn), - get('number_of_sections_per_neurite', self.ref_nrn)) - - for t in NeuriteType: - _equal(_nrt.number_of_sections_per_neurite(self.sec_nrn, neurite_type=t), - get('number_of_sections_per_neurite', self.ref_nrn, neurite_type=t)) - - def test_get_n_segments(self): - assert _nrt.n_segments(self.sec_nrn) == get('number_of_segments', self.ref_nrn)[0] - for t in NeuriteType: - assert (_nrt.n_segments(self.sec_nrn, neurite_type=t) == - get('number_of_segments', self.ref_nrn, neurite_type=t)[0]) - - def test_get_number_of_neurites(self): - assert _nrt.n_neurites(self.sec_nrn) == get('number_of_neurites', self.ref_nrn)[0] - for t in NeuriteType: - assert (_nrt.n_neurites(self.sec_nrn, neurite_type=t) == - get('number_of_neurites', self.ref_nrn, neurite_type=t)[0]) - - def test_get_section_path_distances(self): - _close(_nrt.section_path_lengths(self.sec_nrn), get('section_path_distances', self.ref_nrn)) - for t in NeuriteType: - _close(_nrt.section_path_lengths(self.sec_nrn, neurite_type=t), - get('section_path_distances', self.ref_nrn, neurite_type=t)) - - pl = [_sec.section_path_length(s) for s in i_chain2(self.sec_nrn_trees)] - _close(pl, get('section_path_distances', self.ref_nrn)) - - def test_get_soma_radius(self): - assert_almost_equal(self.sec_nrn.soma.radius, get('soma_radii', self.ref_nrn)[0]) - - def test_get_soma_surface_area(self): - assert_almost_equal(_nrn.soma_surface_area(self.sec_nrn), get('soma_surface_areas', self.ref_nrn)[0]) - - def test_get_soma_volume(self): - assert_almost_equal(_nrn.soma_volume(self.sec_nrn), get('soma_volumes', self.ref_nrn)[0]) - - def test_get_local_bifurcation_angles(self): - _close(_nrt.local_bifurcation_angles(self.sec_nrn), - get('local_bifurcation_angles', self.ref_nrn)) - - for t in NeuriteType: - _close(_nrt.local_bifurcation_angles(self.sec_nrn, neurite_type=t), - get('local_bifurcation_angles', self.ref_nrn, neurite_type=t)) - - ba = [_bf.local_bifurcation_angle(b) - for b in i_chain2(self.sec_nrn_trees, iterator_type=Section.ibifurcation_point)] - - _close(ba, get('local_bifurcation_angles', self.ref_nrn)) - - def test_get_remote_bifurcation_angles(self): - _close(_nrt.remote_bifurcation_angles(self.sec_nrn), - get('remote_bifurcation_angles', self.ref_nrn)) - - for t in NeuriteType: - _close(_nrt.remote_bifurcation_angles(self.sec_nrn, neurite_type=t), - get('remote_bifurcation_angles', self.ref_nrn, neurite_type=t)) - - ba = [_bf.remote_bifurcation_angle(b) - for b in i_chain2(self.sec_nrn_trees, iterator_type=Section.ibifurcation_point)] - - _close(ba, get('remote_bifurcation_angles', self.ref_nrn)) - - def test_get_section_radial_distances(self): - _close(_nrt.section_radial_distances(self.sec_nrn), - get('section_radial_distances', self.ref_nrn)) - - for t in NeuriteType: - _close(_nrt.section_radial_distances(self.sec_nrn, neurite_type=t), - get('section_radial_distances', self.ref_nrn, neurite_type=t)) - - def test_get_trunk_origin_radii(self): - assert_almost_equal(_nrn.trunk_origin_radii(self.sec_nrn), - get('trunk_origin_radii', self.ref_nrn)) - for t in NeuriteType: - assert_almost_equal(_nrn.trunk_origin_radii(self.sec_nrn, neurite_type=t), - get('trunk_origin_radii', self.ref_nrn, neurite_type=t)) - - def test_get_trunk_section_lengths(self): - _close(_nrn.trunk_section_lengths(self.sec_nrn), get('trunk_section_lengths', self.ref_nrn)) - for t in NeuriteType: - _close(_nrn.trunk_section_lengths(self.sec_nrn, neurite_type=t), - get('trunk_section_lengths', self.ref_nrn, neurite_type=t)) - - -class TestH5V1(SectionTreeBase): - - def setup_method(self): - super(TestH5V1, self).setup_method() - self.sec_nrn = nm.load_neuron(Path(H5V1_DATA_PATH, MORPH_FILENAME)) - self.sec_nrn_trees = [n.root_node for n in self.sec_nrn.neurites] - - # Overriding soma values as the same soma points in SWC and ASC have different - # meanings. Hence leading to different values - def test_get_soma_radius(self): - assert_almost_equal(self.sec_nrn.soma.radius, 0.09249506049313666) - - def test_get_soma_surface_area(self): - assert_almost_equal(_nrn.soma_surface_area(self.sec_nrn), - 0.1075095256160432) - - def test_get_soma_volume(self): - with warnings.catch_warnings(record=True): - assert_almost_equal(_nrn.soma_volume(self.sec_nrn), 0.0033147000251481135) - - -class TestSWC(SectionTreeBase): - - def setup_method(self): - self.ref_nrn = 'swc' - self.sec_nrn = nm.load_neuron(Path(SWC_DATA_PATH, SWC_MORPH_FILENAME)) - self.sec_nrn_trees = [n.root_node for n in self.sec_nrn.neurites] - self.ref_types = [NeuriteType.axon, - NeuriteType.basal_dendrite, - NeuriteType.basal_dendrite, - NeuriteType.apical_dendrite, - ] diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index 903b7df1c..acc80b502 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -26,23 +26,19 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Test neurom.features_get features.""" - +"""Test ``neurom.features.get`` function.""" +import itertools import math from io import StringIO from pathlib import Path import neurom as nm import numpy as np -from mock import patch -from neurom import features, iter_neurites, iter_sections, load_neuron, load_neurons +from neurom import features, iter_sections, load_morphology, load_morphologies from neurom.core.population import Population from neurom.core.types import NeuriteType -from neurom.core.types import tree_type_checker as _is_type from neurom.exceptions import NeuroMError -from neurom.features import NEURITEFEATURES -from neurom.features import get as get_feature -from neurom.features import neuritefunc as nf, neuronfunc +from neurom.features import neurite, NameSpace import pytest from numpy.testing import assert_allclose @@ -50,873 +46,713 @@ DATA_PATH = Path(__file__).parent.parent / 'data' NRN_FILES = [DATA_PATH / 'h5/v1' / f for f in ('Neuron.h5', 'Neuron_2_branch.h5', 'bio_neuron-001.h5')] -POP = load_neurons(NRN_FILES) +POP = load_morphologies(NRN_FILES) NRN = POP[0] SWC_PATH = DATA_PATH / 'swc' NEURON_PATH = SWC_PATH / 'Neuron.swc' -NEURON = load_neuron(NEURON_PATH) +NEURON = load_morphology(NEURON_PATH) NEURITES = (NeuriteType.axon, NeuriteType.apical_dendrite, NeuriteType.basal_dendrite, NeuriteType.all) -def assert_items_equal(a, b): - assert sorted(a) == sorted(b) - - -def assert_features_for_neurite(feat, neurons, expected, exact=True): - for neurite_type, expected_values in expected.items(): - if neurite_type is None: - res_pop = get_feature(feat, neurons) - res = get_feature(feat, neurons[0]) - else: - res_pop = get_feature(feat, neurons, neurite_type=neurite_type) - res = get_feature(feat, neurons[0], neurite_type=neurite_type) +def _stats(seq): + seq = list(itertools.chain(*seq)) if isinstance(seq[0], list) else seq + return np.min(seq), np.max(seq), np.sum(seq), np.mean(seq) - if exact: - assert_items_equal(res_pop, expected_values) - else: - assert_allclose(res_pop, expected_values) - # test for single neuron - if isinstance(res, np.ndarray): - # some features, ex: total_length return arrays w/ one element when - # called on a single neuron - assert len(res) == 1 - res = res[0] - if exact: - assert res == expected_values[0] - else: - assert_allclose(res, expected_values[0]) +def test_get_raises(): + with pytest.raises(NeuroMError, + match='Only Neurite, Morphology, Population or list, tuple of Neurite, Morphology'): + features.get('soma_radius', (n for n in POP)) + with pytest.raises(NeuroMError, match='Cant apply "invalid" feature'): + features.get('invalid', NRN) -def _stats(seq): - return np.min(seq), np.max(seq), np.sum(seq), np.mean(seq) +def test_register_existing_feature(): + with pytest.raises(NeuroMError): + features._register_feature(NameSpace.NEURITE, 'total_area', lambda n: None, ()) + with pytest.raises(NeuroMError): + features._register_feature(NameSpace.NEURON, 'total_length_per_neurite', lambda n: None, ()) + with pytest.raises(NeuroMError): + features._register_feature(NameSpace.POPULATION, 'sholl_frequency', lambda n: None, ()) def test_number_of_sections(): - feat = 'number_of_sections' - expected = {None: [84, 42, 202], - NeuriteType.all: [84, 42, 202], - NeuriteType.axon: [21, 21, 179], - NeuriteType.apical_dendrite: [21, 0, 0], - NeuriteType.basal_dendrite: [42, 21, 23], - } - assert_features_for_neurite(feat, POP, expected) - - -def test_max_radial_distances(): - feat = 'max_radial_distances' - expected = { - None: [99.58945832, 94.43342439, 1053.77939245], - NeuriteType.all: [99.58945832, 94.43342439, 1053.77939245], - NeuriteType.axon: [82.442545, 82.442545, 1053.779392], - NeuriteType.basal_dendrite: [94.43342563, 94.43342439, 207.56977859], - } - assert_features_for_neurite(feat, POP, expected, exact=False) - - # Test with a list of neurites - expected = { - None: [99.58945832], - NeuriteType.all: [99.58945832], - NeuriteType.apical_dendrite: [99.589458], - } - assert_features_for_neurite(feat, NRN.neurites, expected, exact=False) + assert features.get('number_of_sections', POP) == [84, 42, 202] + assert features.get('number_of_sections', POP, + neurite_type=NeuriteType.all) == [84, 42, 202] + assert features.get('number_of_sections', POP, + neurite_type=NeuriteType.axon) == [21, 21, 179] + assert features.get('number_of_sections', POP, + neurite_type=NeuriteType.apical_dendrite) == [21, 0, 0] + assert features.get('number_of_sections', POP, + neurite_type=NeuriteType.basal_dendrite) == [42, 21, 23] + + assert features.get('number_of_sections', NEURON) == 84 + assert features.get('number_of_sections', NEURON, + neurite_type=NeuriteType.all) == 84 + assert features.get('number_of_sections', NEURON, + neurite_type=NeuriteType.axon) == 21 + assert features.get('number_of_sections', NEURON, + neurite_type=NeuriteType.basal_dendrite) == 42 + assert features.get('number_of_sections', NEURON, + neurite_type=NeuriteType.apical_dendrite) == 21 + + assert features.get('number_of_sections', NEURON.neurites) == [21, 21, 21, 21] + assert features.get('number_of_sections', NEURON.neurites[0]) == 21 + + assert features.get('number_of_sections', NEURON, neurite_type=NeuriteType.soma) == 0 + assert features.get('number_of_sections', NEURON, neurite_type=NeuriteType.undefined) == 0 def test_max_radial_distance(): - feat = 'max_radial_distance' - expected = { - None: 99.58945832, - NeuriteType.all: 99.58945832, - NeuriteType.apical_dendrite: 99.589458, - } - - for neurite_type, expected_values in expected.items(): - if neurite_type is None: - res = get_feature(feat, NRN) - else: - res = get_feature(feat, NRN, neurite_type=neurite_type) - assert_allclose(res, expected_values) - - -def test_section_tortuosity_pop(): - - feat = 'section_tortuosity' - - assert_allclose(_stats(get_feature(feat, POP)), - (1.0, - 4.657, - 440.408, - 1.342), - rtol=1e-3) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.all)), - (1.0, - 4.657, - 440.408, - 1.342), - rtol=1e-3) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.apical_dendrite)), - (1.070, - 1.573, - 26.919, - 1.281), - rtol=1e-3) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.basal_dendrite)), - (1.042, - 1.674, - 106.596, - 1.239), - rtol=1e-3) - - -def test_section_tortuosity_nrn(): - feat = 'section_tortuosity' - assert_allclose(_stats(get_feature(feat, NRN)), - (1.070, - 1.573, - 106.424, - 1.266), - rtol=1e-3) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.all)), - (1.070, - 1.573, - 106.424, - 1.266), - rtol=1e-3) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.apical_dendrite)), - (1.070, - 1.573, - 26.919, - 1.281), - rtol=1e-3) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.basal_dendrite)), - (1.078, - 1.550, - 51.540, - 1.227), - rtol=1e-3) + assert_allclose( + features.get('max_radial_distance', POP), + [99.58945832, 94.43342439, 1053.77939245]) + assert_allclose( + features.get('max_radial_distance', POP, neurite_type=NeuriteType.all), + [99.58945832, 94.43342439, 1053.77939245]) + assert_allclose( + features.get('max_radial_distance', POP, neurite_type=NeuriteType.axon), + [82.442545, 82.442545, 1053.779392]) + assert_allclose( + features.get('max_radial_distance', POP, neurite_type=NeuriteType.basal_dendrite), + [94.43342563, 94.43342439, 207.56977859]) + + assert_allclose( + features.get('max_radial_distance', NRN), 99.58945832) + assert_allclose( + features.get('max_radial_distance', NRN, neurite_type=NeuriteType.all), 99.58945832) + assert_allclose(features.get( + 'max_radial_distance', NRN, neurite_type=NeuriteType.apical_dendrite), 99.589458) + + assert_allclose( + features.get('max_radial_distance', NRN.neurites), + [99.58946, 80.05163, 94.433426, 82.44254]) + assert_allclose( + features.get('max_radial_distance', NRN.neurites[0]), 99.58946) + + +def test_section_tortuosity(): + assert_allclose( + _stats(features.get('section_tortuosity', POP)), + (1.0, 4.657, 440.408, 1.342), rtol=1e-3) + assert_allclose( + _stats(features.get('section_tortuosity', POP, neurite_type=NeuriteType.all)), + (1.0, 4.657, 440.408, 1.342), rtol=1e-3) + assert_allclose( + _stats(features.get('section_tortuosity', POP, neurite_type=NeuriteType.apical_dendrite)), + (1.070, 1.573, 26.919, 1.281), rtol=1e-3) + assert_allclose( + _stats(features.get('section_tortuosity', POP, neurite_type=NeuriteType.basal_dendrite)), + (1.042, 1.674, 106.596, 1.239), rtol=1e-3) + + assert_allclose( + _stats(features.get('section_tortuosity', NRN)), + (1.070, 1.573, 106.424, 1.266), rtol=1e-3) + assert_allclose( + _stats(features.get('section_tortuosity', NRN, neurite_type=NeuriteType.all)), + (1.070, 1.573, 106.424, 1.266), rtol=1e-3) + assert_allclose( + _stats(features.get('section_tortuosity', NRN, neurite_type=NeuriteType.apical_dendrite)), + (1.070, 1.573, 26.919, 1.281), rtol=1e-3) + assert_allclose( + _stats(features.get('section_tortuosity', NRN, neurite_type=NeuriteType.basal_dendrite)), + (1.078, 1.550, 51.540, 1.227), rtol=1e-3) def test_number_of_segments(): - - feat = 'number_of_segments' - - expected = {None: [840, 419, 5179], - NeuriteType.all: [840, 419, 5179], - NeuriteType.axon: [210, 209, 4508], - NeuriteType.apical_dendrite: [210, 0, 0], - NeuriteType.basal_dendrite: [420, 210, 671], - } - - assert_features_for_neurite(feat, POP, expected) - - -def test_number_of_neurites_pop(): - feat = 'number_of_neurites' - expected = {None: [4, 2, 4], - NeuriteType.all: [4, 2, 4], - NeuriteType.axon: [1, 1, 1], - NeuriteType.apical_dendrite: [1, 0, 0], - NeuriteType.basal_dendrite: [2, 1, 3], - } - assert_features_for_neurite(feat, POP, expected) - - -def test_number_of_bifurcations_pop(): - feat = 'number_of_bifurcations' - expected = {None: [40, 20, 97], - NeuriteType.all: [40, 20, 97], - NeuriteType.axon: [10, 10, 87], - NeuriteType.apical_dendrite: [10, 0, 0], - NeuriteType.basal_dendrite: [20, 10, 10], - } - assert_features_for_neurite(feat, POP, expected) - - -def test_number_of_forking_points_pop(): - - feat = 'number_of_forking_points' - - expected = {None: [40, 20, 98], - NeuriteType.all: [40, 20, 98], - NeuriteType.axon: [10, 10, 88], - NeuriteType.apical_dendrite: [10, 0, 0], - NeuriteType.basal_dendrite: [20, 10, 10], - } - assert_features_for_neurite(feat, POP, expected) - - -def test_number_of_terminations_pop(): - feat = 'number_of_terminations' - expected = {None: [44, 22, 103], - NeuriteType.all: [44, 22, 103], - NeuriteType.axon: [11, 11, 90], - NeuriteType.apical_dendrite: [11, 0, 0], - NeuriteType.basal_dendrite: [22, 11, 13], - } - assert_features_for_neurite(feat, POP, expected) - - -def test_total_length_pop(): - feat = 'total_length' - expected = {None: [840.68522362011538, 418.83424432013902, 13250.825773939932], - NeuriteType.all: [840.68522362011538, 418.83424432013902, 13250.825773939932], - NeuriteType.axon: [207.8797736031714, 207.81088341560977, 11767.156115224638], - NeuriteType.apical_dendrite: [214.37302709169489, 0, 0], - NeuriteType.basal_dendrite: [418.43242292524889, 211.02336090452931, 1483.6696587152967], - } - assert_features_for_neurite(feat, POP, expected, exact=False) - - -def test_segment_radii_pop(): - - feat = 'segment_radii' - - assert_allclose(_stats(get_feature(feat, POP)), - (0.079999998211860657, - 1.2150000333786011, - 1301.9191725363567, - 0.20222416473071708)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.all)), - (0.079999998211860657, - 1.2150000333786011, - 1301.9191725363567, - 0.20222416473071708)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.apical_dendrite)), - (0.13142434507608414, - 1.0343990325927734, - 123.41135908663273, - 0.58767313850777492)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.basal_dendrite)), - (0.079999998211860657, - 1.2150000333786011, - 547.43900821779164, - 0.42078324997524336)) - - -def test_segment_radii_nrn(): - - feat = 'segment_radii' - - assert_allclose(_stats(get_feature(feat, NRN)), - (0.12087134271860123, - 1.0343990325927734, - 507.01994501426816, - 0.60359517263603357)) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.all)), - (0.12087134271860123, - 1.0343990325927734, - 507.01994501426816, - 0.60359517263603357)) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.apical_dendrite)), - (0.13142434507608414, - 1.0343990325927734, - 123.41135908663273, - 0.58767313850777492)) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.basal_dendrite)), - (0.14712842553853989, - 1.0215770602226257, - 256.71241207793355, - 0.61122002875698467)) - - -def test_segment_meander_angles_pop(): - - feat = 'segment_meander_angles' - - assert_allclose(_stats(get_feature(feat, POP)), - (0.0, 3.1415, 14637.9776, 2.3957), - rtol=1e-3) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.all)), - (0.0, 3.1415, 14637.9776, 2.3957), - rtol=1e-3) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.apical_dendrite)), - (0.3261, 3.0939, 461.9816, 2.4443), - rtol=1e-4) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.basal_dendrite)), - (0.0, 3.1415, 2926.2411, 2.4084), - rtol=1e-4) - - -def test_segment_meander_angles_nrn(): - - feat = 'segment_meander_angles' - assert_allclose(_stats(get_feature(feat, NRN)), - (0.32610, 3.12996, 1842.35, 2.43697), - rtol=1e-5) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.all)), - (0.32610, 3.12996, 1842.35, 2.43697), - rtol=1e-5) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.apical_dendrite)), - (0.32610, 3.09392, 461.981, 2.44434), - rtol=1e-5) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.basal_dendrite)), - (0.47318, 3.12996, 926.338, 2.45063), - rtol=1e-4) - - -def test_neurite_volumes_nrn(): - - feat = 'neurite_volumes' - - assert_allclose(_stats(get_feature(feat, NRN)), - (271.9412, 281.2475, 1104.907, 276.2269), - rtol=1e-5) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.all)), - (271.9412, 281.2475, 1104.907, 276.2269), - rtol=1e-5) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.axon)), - (276.7386, 276.7386, 276.7386, 276.7386), - rtol=1e-5) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.basal_dendrite)), - (274.9803, 281.2475, 556.2279, 278.1139), - rtol=1e-5) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.apical_dendrite)), - (271.9412, 271.9412, 271.9412, 271.9412), - rtol=1e-5) - - -def test_neurite_volumes_pop(): - - feat = 'neurite_volumes' - - assert_allclose(_stats(get_feature(feat, POP)), - (28.356406629821159, 281.24754646913954, 2249.4613918388391, 224.9461391838839)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.all)), - (28.356406629821159, 281.24754646913954, 2249.4613918388391, 224.9461391838839)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.axon)), - (276.58135508666612, 277.5357232437392, 830.85568094763551, 276.95189364921185)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.basal_dendrite)), - (28.356406629821159, 281.24754646913954, 1146.6644894516851, 191.1107482419475)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.apical_dendrite)), - (271.94122143951864, 271.94122143951864, 271.94122143951864, 271.94122143951864)) - - -def test_neurite_density_nrn(): - - feat = 'neurite_volume_density' - - assert_allclose(_stats(get_feature(feat, NRN)), - (0.24068543213643726, 0.52464681266899216, 1.4657913638494682, 0.36644784096236704)) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.all)), - (0.24068543213643726, 0.52464681266899216, 1.4657913638494682, 0.36644784096236704)) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.axon)), - (0.26289304906104355, 0.26289304906104355, 0.26289304906104355, 0.26289304906104355)) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.basal_dendrite)), - (0.24068543213643726, 0.52464681266899216, 0.76533224480542938, 0.38266612240271469)) - - assert_allclose(_stats(get_feature(feat, NRN, neurite_type=NeuriteType.apical_dendrite)), - (0.43756606998299519, 0.43756606998299519, 0.43756606998299519, 0.43756606998299519)) - - -def test_neurite_density_pop(): - - feat = 'neurite_volume_density' - - assert_allclose(_stats(get_feature(feat, POP)), - (6.1847539631150784e-06, 0.52464681266899216, 1.9767794901940539, 0.19767794901940539)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.all)), - (6.1847539631150784e-06, 0.52464681266899216, 1.9767794901940539, 0.19767794901940539)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.axon)), - (6.1847539631150784e-06, 0.26465213325053372, 0.5275513670655404, 0.17585045568851346), - rtol=1e-6) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.basal_dendrite)), - (0.00034968816544949771, 0.52464681266899216, 1.0116620531455183, 0.16861034219091972)) - - assert_allclose(_stats(get_feature(feat, POP, neurite_type=NeuriteType.apical_dendrite)), - (0.43756606998299519, 0.43756606998299519, 0.43756606998299519, 0.43756606998299519)) + assert features.get('number_of_segments', POP) == [840, 419, 5179] + assert features.get('number_of_segments', POP, + neurite_type=NeuriteType.all) == [840, 419, 5179] + assert features.get('number_of_segments', POP, + neurite_type=NeuriteType.axon) == [210, 209, 4508] + assert features.get('number_of_segments', POP, + neurite_type=NeuriteType.apical_dendrite) == [210, 0, 0] + assert features.get('number_of_segments', POP, + neurite_type=NeuriteType.basal_dendrite) == [420, 210, 671] + + assert features.get('number_of_segments', NRN) == 840 + assert features.get('number_of_segments', NRN, + neurite_type=NeuriteType.all) == 840 + assert features.get('number_of_segments', NRN, + neurite_type=NeuriteType.axon) == 210 + assert features.get('number_of_segments', NRN, + neurite_type=NeuriteType.apical_dendrite) == 210 + assert features.get('number_of_segments', NRN, + neurite_type=NeuriteType.basal_dendrite) == 420 + + +def test_number_of_neurites(): + assert features.get('number_of_neurites', POP) == [4, 2, 4] + assert features.get('number_of_neurites', POP, + neurite_type=NeuriteType.all) == [4, 2, 4] + assert features.get('number_of_neurites', POP, + neurite_type=NeuriteType.axon) == [1, 1, 1] + assert features.get('number_of_neurites', POP, + neurite_type=NeuriteType.apical_dendrite) == [1, 0, 0] + assert features.get('number_of_neurites', POP, + neurite_type=NeuriteType.basal_dendrite) == [2, 1, 3] + + assert features.get('number_of_neurites', NRN) == 4 + assert features.get('number_of_neurites', NRN, + neurite_type=NeuriteType.all) == 4 + assert features.get('number_of_neurites', NRN, + neurite_type=NeuriteType.axon) == 1 + assert features.get('number_of_neurites', NRN, + neurite_type=NeuriteType.apical_dendrite) == 1 + assert features.get('number_of_neurites', NRN, + neurite_type=NeuriteType.basal_dendrite) == 2 + + +def test_number_of_bifurcations(): + assert features.get('number_of_bifurcations', POP) == [40, 20, 97] + assert features.get('number_of_bifurcations', POP, + neurite_type=NeuriteType.all) == [40, 20, 97] + assert features.get('number_of_bifurcations', POP, + neurite_type=NeuriteType.axon) == [10, 10, 87] + assert features.get('number_of_bifurcations', POP, + neurite_type=NeuriteType.apical_dendrite) == [10, 0, 0] + assert features.get('number_of_bifurcations', POP, + neurite_type=NeuriteType.basal_dendrite) == [20, 10, 10] + + assert features.get('number_of_bifurcations', NRN) == 40 + assert features.get('number_of_bifurcations', NRN, + neurite_type=NeuriteType.all) == 40 + assert features.get('number_of_bifurcations', NRN, + neurite_type=NeuriteType.axon) == 10 + assert features.get('number_of_bifurcations', NRN, + neurite_type=NeuriteType.apical_dendrite) == 10 + assert features.get('number_of_bifurcations', NRN, + neurite_type=NeuriteType.basal_dendrite) == 20 + + +def test_number_of_forking_points(): + assert features.get('number_of_forking_points', POP) == [40, 20, 98] + assert features.get('number_of_forking_points', POP, + neurite_type=NeuriteType.all) == [40, 20, 98] + assert features.get('number_of_forking_points', POP, + neurite_type=NeuriteType.axon) == [10, 10, 88] + assert features.get('number_of_forking_points', POP, + neurite_type=NeuriteType.apical_dendrite) == [10, 0, 0] + assert features.get('number_of_forking_points', POP, + neurite_type=NeuriteType.basal_dendrite) == [20, 10, 10] + + assert features.get('number_of_forking_points', NRN) == 40 + assert features.get('number_of_forking_points', NRN, + neurite_type=NeuriteType.all) == 40 + assert features.get('number_of_forking_points', NRN, + neurite_type=NeuriteType.axon) == 10 + assert features.get('number_of_forking_points', NRN, + neurite_type=NeuriteType.apical_dendrite) == 10 + assert features.get('number_of_forking_points', NRN, + neurite_type=NeuriteType.basal_dendrite) == 20 + + +def test_number_of_leaves(): + assert features.get('number_of_leaves', POP) == [44, 22, 103] + assert features.get('number_of_leaves', POP, + neurite_type=NeuriteType.all) == [44, 22, 103] + assert features.get('number_of_leaves', POP, + neurite_type=NeuriteType.axon) == [11, 11, 90] + assert features.get('number_of_leaves', POP, + neurite_type=NeuriteType.apical_dendrite) == [11, 0, 0] + assert features.get('number_of_leaves', POP, + neurite_type=NeuriteType.basal_dendrite) == [22, 11, 13] + + assert features.get('number_of_leaves', NRN) == 44 + assert features.get('number_of_leaves', NRN, + neurite_type=NeuriteType.all) == 44 + assert features.get('number_of_leaves', NRN, + neurite_type=NeuriteType.axon) == 11 + assert features.get('number_of_leaves', NRN, + neurite_type=NeuriteType.apical_dendrite) == 11 + assert features.get('number_of_leaves', NRN, + neurite_type=NeuriteType.basal_dendrite) == 22 + + +def test_total_length(): + assert_allclose( + features.get('total_length', POP), + [840.68522362011538, 418.83424432013902, 13250.825773939932]) + assert_allclose( + features.get('total_length', POP, neurite_type=NeuriteType.all), + [840.68522362011538, 418.83424432013902, 13250.825773939932]) + assert_allclose( + features.get('total_length', POP, neurite_type=NeuriteType.axon), + [207.8797736031714, 207.81088341560977, 11767.156115224638]) + assert_allclose( + features.get('total_length', POP, neurite_type=NeuriteType.apical_dendrite), + [214.37302709169489, 0, 0]) + assert_allclose( + features.get('total_length', POP, neurite_type=NeuriteType.basal_dendrite), + [418.43242292524889, 211.02336090452931, 1483.6696587152967]) + + assert_allclose( + features.get('total_length', NEURON, neurite_type=NeuriteType.axon), + 207.87975221) + assert_allclose( + features.get('total_length', NEURON, neurite_type=NeuriteType.basal_dendrite), + 418.432424) + assert_allclose( + features.get('total_length', NEURON, neurite_type=NeuriteType.apical_dendrite), + 214.37304578) + assert_allclose( + features.get('total_length', NEURON, neurite_type=NeuriteType.axon), + 207.87975221) + assert_allclose( + features.get('total_length', NEURON, neurite_type=NeuriteType.basal_dendrite), + 418.43241644) + assert_allclose( + features.get('total_length', NEURON, neurite_type=NeuriteType.apical_dendrite), + 214.37304578) + + +def test_neurite_lengths(): + actual = features.get('total_length_per_neurite', POP, neurite_type=NeuriteType.basal_dendrite) + expected = [207.31504917144775, 211.11737489700317, 211.02336168289185, + 501.28893661499023, 133.21348762512207, 849.1672043800354] + for a,e in zip(actual, expected): + assert_allclose(a, e) + + assert_allclose( + features.get('total_length_per_neurite', NEURON, neurite_type=NeuriteType.axon), + (207.87975221,)) + assert_allclose( + features.get('total_length_per_neurite', NEURON, neurite_type=NeuriteType.basal_dendrite), + (211.11737442, 207.31504202)) + assert_allclose( + features.get('total_length_per_neurite', NEURON, neurite_type=NeuriteType.apical_dendrite), + (214.37304578,)) + + +def test_segment_radii(): + assert_allclose( + _stats(features.get('segment_radii', POP)), + (0.079999998211860657, 1.2150000333786011, 1301.9191725363567, 0.20222416473071708)) + assert_allclose( + _stats(features.get('segment_radii', POP, neurite_type=NeuriteType.all)), + (0.079999998211860657, 1.2150000333786011, 1301.9191725363567, 0.20222416473071708)) + assert_allclose( + _stats(features.get('segment_radii', POP, neurite_type=NeuriteType.apical_dendrite)), + (0.13142434507608414, 1.0343990325927734, 123.41135908663273, 0.58767313850777492)) + assert_allclose( + _stats(features.get('segment_radii', POP, neurite_type=NeuriteType.basal_dendrite)), + (0.079999998211860657, 1.2150000333786011, 547.43900821779164, 0.42078324997524336)) + + assert_allclose( + _stats(features.get('segment_radii', NRN)), + (0.12087134271860123, 1.0343990325927734, 507.01994501426816, 0.60359517263603357)) + assert_allclose( + _stats(features.get('segment_radii', NRN, neurite_type=NeuriteType.all)), + (0.12087134271860123, 1.0343990325927734, 507.01994501426816, 0.60359517263603357)) + assert_allclose( + _stats(features.get('segment_radii', NRN, neurite_type=NeuriteType.apical_dendrite)), + (0.13142434507608414, 1.0343990325927734, 123.41135908663273, 0.58767313850777492)) + assert_allclose( + _stats(features.get('segment_radii', NRN, neurite_type=NeuriteType.basal_dendrite)), + (0.14712842553853989, 1.0215770602226257, 256.71241207793355, 0.61122002875698467)) + + +def test_segment_meander_angles(): + assert_allclose( + _stats(features.get('segment_meander_angles', POP)), + (0.0, 3.1415, 14637.9776, 2.3957), rtol=1e-3) + assert_allclose( + _stats(features.get('segment_meander_angles', POP, neurite_type=NeuriteType.all)), + (0.0, 3.1415, 14637.9776, 2.3957), rtol=1e-3) + assert_allclose( + _stats(features.get('segment_meander_angles', POP, neurite_type=NeuriteType.apical_dendrite)), + (0.3261, 3.0939, 461.9816, 2.4443), rtol=1e-4) + assert_allclose( + _stats(features.get('segment_meander_angles', POP, neurite_type=NeuriteType.basal_dendrite)), + (0.0, 3.1415, 2926.2411, 2.4084), rtol=1e-4) + + assert_allclose( + _stats(features.get('segment_meander_angles', NRN)), + (0.32610, 3.12996, 1842.35, 2.43697), rtol=1e-5) + assert_allclose( + _stats(features.get('segment_meander_angles', NRN, neurite_type=NeuriteType.all)), + (0.32610, 3.12996, 1842.35, 2.43697), rtol=1e-5) + assert_allclose( + _stats(features.get('segment_meander_angles', NRN, neurite_type=NeuriteType.apical_dendrite)), + (0.32610, 3.09392, 461.981, 2.44434), rtol=1e-5) + assert_allclose( + _stats(features.get('segment_meander_angles', NRN, neurite_type=NeuriteType.basal_dendrite)), + (0.47318, 3.12996, 926.338, 2.45063), rtol=1e-4) def test_segment_meander_angles_single_section(): - feat = 'segment_meander_angles' - - nrn = nm.load_neuron(StringIO(u"""((CellBody) (0 0 0 0)) + m = nm.load_morphology(StringIO(u"""((CellBody) (0 0 0 0)) ((Dendrite) (0 0 0 2) (1 0 0 2) (1 1 0 2) (2 1 0 2) (2 2 0 2)))"""), reader='asc') - - nrt = nrn.neurites[0] - pop = Population([nrn]) + nrt = m.neurites[0] + pop = [m] ref = [math.pi / 2, math.pi / 2, math.pi / 2] - assert ref == get_feature(feat, nrt).tolist() - assert ref == get_feature(feat, nrn).tolist() - assert ref == get_feature(feat, pop).tolist() - - -def test_neurite_features_accept_single_tree(): - for f in NEURITEFEATURES: - ret = get_feature(f, NRN.neurites[0]) - if isinstance(ret, np.ndarray): - assert ret.dtype.kind in ('i', 'f') - assert len(features._find_feature_func(f).shape) >= 1 - assert len(ret) > 0 - else: - assert np.isscalar(ret) - - -@patch.dict(NEURITEFEATURES) -def test_register_neurite_feature_nrns(): - - def npts(neurite): - return len(neurite.points) - - def vol(neurite): - return neurite.volume - - features.register_neurite_feature('foo', npts) - - n_points_ref = [len(n.points) for n in iter_neurites(POP)] - n_points = get_feature('foo', POP) - assert_items_equal(n_points, n_points_ref) - - # test neurite type filtering - n_points_ref = [len(n.points) for n in iter_neurites(POP, filt=_is_type(NeuriteType.axon))] - n_points = get_feature('foo', POP, neurite_type=NeuriteType.axon) - assert_items_equal(n_points, n_points_ref) - - features.register_neurite_feature('bar', vol) - - n_volume_ref = [n.volume for n in iter_neurites(POP)] - n_volume = get_feature('bar', POP) - assert_items_equal(n_volume, n_volume_ref) - - # test neurite type filtering - n_volume_ref = [n.volume for n in iter_neurites(POP, filt=_is_type(NeuriteType.axon))] - n_volume = get_feature('bar', POP, neurite_type=NeuriteType.axon) - assert_items_equal(n_volume, n_volume_ref) - - -@patch.dict(NEURITEFEATURES) -def test_register_neurite_feature_pop(): - - def npts(neurite): - return len(neurite.points) - - def vol(neurite): - return neurite.volume - - features.register_neurite_feature('foo', npts) - - n_points_ref = [len(n.points) for n in iter_neurites(POP)] - n_points = get_feature('foo', POP) - assert_items_equal(n_points, n_points_ref) - - # test neurite type filtering - n_points_ref = [len(n.points) for n in iter_neurites(POP, - filt=_is_type(NeuriteType.basal_dendrite))] - n_points = get_feature('foo', POP, neurite_type=NeuriteType.basal_dendrite) - assert_items_equal(n_points, n_points_ref) - - features.register_neurite_feature('bar', vol) - - n_volume_ref = [n.volume for n in iter_neurites(POP)] - n_volume = get_feature('bar', POP) - assert_items_equal(n_volume, n_volume_ref) - - # test neurite type filtering - n_volume_ref = [n.volume for n in iter_neurites(POP, filt=_is_type(NeuriteType.basal_dendrite))] - n_volume = get_feature('bar', POP, neurite_type=NeuriteType.basal_dendrite) - assert_items_equal(n_volume, n_volume_ref) - - -def test_get_raises(): - with pytest.raises(NeuroMError): - get_feature('ahah-I-do-not-exist!', lambda n: None) - - -def test_register_existing_feature_raises(): - with pytest.raises(NeuroMError): - features.register_neurite_feature('section_areas', lambda n: None) + assert ref == features.get('segment_meander_angles', nrt) + assert ref == features.get('segment_meander_angles', m) + assert ref == features.get('segment_meander_angles', pop) + + +def test_neurite_volumes(): + assert_allclose( + _stats(features.get('total_volume_per_neurite', POP)), + (28.356406629821159, 281.24754646913954, 2249.4613918388391, 224.9461391838839)) + assert_allclose( + _stats(features.get('total_volume_per_neurite', POP, neurite_type=NeuriteType.all)), + (28.356406629821159, 281.24754646913954, 2249.4613918388391, 224.9461391838839)) + assert_allclose( + _stats(features.get('total_volume_per_neurite', POP, neurite_type=NeuriteType.axon)), + (276.58135508666612, 277.5357232437392, 830.85568094763551, 276.95189364921185)) + assert_allclose( + _stats(features.get('total_volume_per_neurite', POP, neurite_type=NeuriteType.apical_dendrite)), + (271.94122143951864, 271.94122143951864, 271.94122143951864, 271.94122143951864)) + assert_allclose( + _stats(features.get('total_volume_per_neurite', POP, neurite_type=NeuriteType.basal_dendrite)), + (28.356406629821159, 281.24754646913954, 1146.6644894516851, 191.1107482419475)) + + assert_allclose( + _stats(features.get('total_volume_per_neurite', NRN)), + (271.9412, 281.2475, 1104.907, 276.2269), rtol=1e-5) + assert_allclose( + _stats(features.get('total_volume_per_neurite', NRN, neurite_type=NeuriteType.all)), + (271.9412, 281.2475, 1104.907, 276.2269), rtol=1e-5) + assert_allclose( + _stats(features.get('total_volume_per_neurite', NRN, neurite_type=NeuriteType.axon)), + (276.7386, 276.7386, 276.7386, 276.7386), rtol=1e-5) + assert_allclose( + _stats(features.get('total_volume_per_neurite', NRN, neurite_type=NeuriteType.apical_dendrite)), + (271.9412, 271.9412, 271.9412, 271.9412), rtol=1e-5) + assert_allclose( + _stats(features.get('total_volume_per_neurite', NRN, neurite_type=NeuriteType.basal_dendrite)), + (274.9803, 281.2475, 556.2279, 278.1139), rtol=1e-5) + + +def test_neurite_density(): + assert_allclose( + _stats(features.get('neurite_volume_density', POP)), + (6.1847539631150784e-06, 0.52464681266899216, 1.9767794901940539, 0.19767794901940539)) + assert_allclose( + _stats(features.get('neurite_volume_density', POP, neurite_type=NeuriteType.all)), + (6.1847539631150784e-06, 0.52464681266899216, 1.9767794901940539, 0.19767794901940539)) + assert_allclose( + _stats(features.get('neurite_volume_density', POP, neurite_type=NeuriteType.axon)), + (6.1847539631150784e-06, 0.26465213325053372, 0.5275513670655404, 0.1758504556885134), 1e-6) + assert_allclose( + _stats(features.get('neurite_volume_density', POP, neurite_type=NeuriteType.apical_dendrite)), + (0.43756606998299519, 0.43756606998299519, 0.43756606998299519, 0.43756606998299519)) + assert_allclose( + _stats(features.get('neurite_volume_density', POP, neurite_type=NeuriteType.basal_dendrite)), + (0.00034968816544949771, 0.52464681266899216, 1.0116620531455183, 0.16861034219091972)) + + assert_allclose( + _stats(features.get('neurite_volume_density', NRN)), + (0.24068543213643726, 0.52464681266899216, 1.4657913638494682, 0.36644784096236704)) + assert_allclose( + _stats(features.get('neurite_volume_density', NRN, neurite_type=NeuriteType.all)), + (0.24068543213643726, 0.52464681266899216, 1.4657913638494682, 0.36644784096236704)) + assert_allclose( + _stats(features.get('neurite_volume_density', NRN, neurite_type=NeuriteType.axon)), + (0.26289304906104355, 0.26289304906104355, 0.26289304906104355, 0.26289304906104355)) + assert_allclose( + _stats(features.get('neurite_volume_density', NRN, neurite_type=NeuriteType.apical_dendrite)), + (0.43756606998299519, 0.43756606998299519, 0.43756606998299519, 0.43756606998299519)) + assert_allclose( + _stats(features.get('neurite_volume_density', NRN, neurite_type=NeuriteType.basal_dendrite)), + (0.24068543213643726, 0.52464681266899216, 0.76533224480542938, 0.38266612240271469)) def test_section_lengths(): ref_seclen = [n.length for n in iter_sections(NEURON)] - seclen = get_feature('section_lengths', NEURON) + seclen = features.get('section_lengths', NEURON) assert len(seclen) == 84 assert_allclose(seclen, ref_seclen) - -def test_section_lengths_axon(): - s = get_feature('section_lengths', NEURON, neurite_type=NeuriteType.axon) + s = features.get('section_lengths', NEURON, neurite_type=NeuriteType.axon) assert len(s) == 21 - - -def test_total_lengths_basal(): - s = get_feature('section_lengths', NEURON, neurite_type=NeuriteType.basal_dendrite) + s = features.get('section_lengths', NEURON, neurite_type=NeuriteType.basal_dendrite) assert len(s) == 42 - - -def test_section_lengths_apical(): - s = get_feature('section_lengths', NEURON, neurite_type=NeuriteType.apical_dendrite) + s = features.get('section_lengths', NEURON, neurite_type=NeuriteType.apical_dendrite) assert len(s) == 21 - -def test_total_length_per_neurite_axon(): - tl = get_feature('total_length_per_neurite', NEURON, neurite_type=NeuriteType.axon) - assert len(tl) == 1 - assert_allclose(tl, (207.87975221,)) - - -def test_total_length_per_neurite_basal(): - tl = get_feature('total_length_per_neurite', NEURON, neurite_type=NeuriteType.basal_dendrite) - assert len(tl) == 2 - assert_allclose(tl, (211.11737442, 207.31504202)) - - -def test_total_length_per_neurite_apical(): - tl = get_feature('total_length_per_neurite', NEURON, neurite_type=NeuriteType.apical_dendrite) - assert len(tl) == 1 - assert_allclose(tl, (214.37304578,)) - - -def test_total_length_axon(): - tl = get_feature('total_length', NEURON, neurite_type=NeuriteType.axon) - assert len(tl) == 1 - assert_allclose(tl, (207.87975221,)) - - -def test_total_length_basal(): - tl = get_feature('total_length', NEURON, neurite_type=NeuriteType.basal_dendrite) - assert len(tl) == 1 - assert_allclose(tl, (418.43241644,)) - - -def test_total_length_apical(): - tl = get_feature('total_length', NEURON, neurite_type=NeuriteType.apical_dendrite) - assert len(tl) == 1 - assert_allclose(tl, (214.37304578,)) - - -def test_section_lengths_invalid(): - s = get_feature('section_lengths', NEURON, neurite_type=NeuriteType.soma) + s = features.get('section_lengths', NEURON, neurite_type=NeuriteType.soma) assert len(s) == 0 - s = get_feature('section_lengths', NEURON, neurite_type=NeuriteType.undefined) + s = features.get('section_lengths', NEURON, neurite_type=NeuriteType.undefined) assert len(s) == 0 -def test_section_path_distances_axon(): - path_lengths = get_feature('section_path_distances', NEURON, neurite_type=NeuriteType.axon) - assert len(path_lengths) == 21 - - -def test_section_path_distances_pop(): - path_distances = get_feature('section_path_distances', POP) +def test_section_path_distances(): + path_distances = features.get('section_path_distances', POP) assert len(path_distances) == 328 - assert sum(len(get_feature('section_path_distances', nrn)) for nrn in POP) == 328 + assert sum(len(features.get('section_path_distances', m)) for m in POP) == 328 + + path_lengths = features.get('section_path_distances', NEURON, neurite_type=NeuriteType.axon) + assert len(path_lengths) == 21 def test_segment_lengths(): - ref_seglen = nf.segment_lengths(NEURON) - seglen = get_feature('segment_lengths', NEURON) + ref_seglen = np.concatenate([neurite.segment_lengths(s) for s in NEURON.neurites]) + seglen = features.get('segment_lengths', NEURON) assert len(seglen) == 840 assert_allclose(seglen, ref_seglen) - seglen = get_feature('segment_lengths', NEURON, neurite_type=NeuriteType.all) + seglen = features.get('segment_lengths', NEURON, neurite_type=NeuriteType.all) assert len(seglen) == 840 assert_allclose(seglen, ref_seglen) def test_local_bifurcation_angles(): - ref_local_bifangles = list(nf.local_bifurcation_angles(NEURON)) + ref_local_bifangles = np.concatenate([neurite.local_bifurcation_angles(s) + for s in NEURON.neurites]) - local_bifangles = get_feature('local_bifurcation_angles', NEURON) + local_bifangles = features.get('local_bifurcation_angles', NEURON) assert len(local_bifangles) == 40 assert_allclose(local_bifangles, ref_local_bifangles) - local_bifangles = get_feature('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.all) + local_bifangles = features.get('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.all) assert len(local_bifangles) == 40 assert_allclose(local_bifangles, ref_local_bifangles) - s = get_feature('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.axon) + s = features.get('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.axon) assert len(s) == 10 - - s = get_feature('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.basal_dendrite) + s = features.get('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.basal_dendrite) assert len(s) == 20 - - s = get_feature('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.apical_dendrite) + s = features.get('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.apical_dendrite) assert len(s) == 10 - -def test_local_bifurcation_angles_invalid(): - s = get_feature('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.soma) + s = features.get('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.soma) assert len(s) == 0 - s = get_feature('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.undefined) + s = features.get('local_bifurcation_angles', NEURON, neurite_type=NeuriteType.undefined) assert len(s) == 0 def test_remote_bifurcation_angles(): - ref_remote_bifangles = list(nf.remote_bifurcation_angles(NEURON)) - remote_bifangles = get_feature('remote_bifurcation_angles', NEURON) + ref_remote_bifangles = np.concatenate([neurite.remote_bifurcation_angles(s) + for s in NEURON.neurites]) + remote_bifangles = features.get('remote_bifurcation_angles', NEURON) assert len(remote_bifangles) == 40 assert_allclose(remote_bifangles, ref_remote_bifangles) - remote_bifangles = get_feature('remote_bifurcation_angles', - NEURON, neurite_type=NeuriteType.all) + remote_bifangles = features.get('remote_bifurcation_angles', + NEURON, neurite_type=NeuriteType.all) assert len(remote_bifangles) == 40 assert_allclose(remote_bifangles, ref_remote_bifangles) - s = get_feature('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.axon) + s = features.get('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.axon) assert len(s) == 10 - - s = get_feature('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.basal_dendrite) + s = features.get('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.basal_dendrite) assert len(s) == 20 - - s = get_feature('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.apical_dendrite) + s = features.get('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.apical_dendrite) assert len(s) == 10 - -def test_remote_bifurcation_angles_invalid(): - s = get_feature('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.soma) + s = features.get('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.soma) assert len(s) == 0 - s = get_feature('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.undefined) + s = features.get('remote_bifurcation_angles', NEURON, neurite_type=NeuriteType.undefined) assert len(s) == 0 def test_segment_radial_distances_origin(): origin = (-100, -200, -300) - ref_segs = nf.segment_radial_distances(NEURON) - ref_segs_origin = nf.segment_radial_distances(NEURON, origin=origin) + ref_segs = np.concatenate([neurite.segment_radial_distances(s) for s in NEURON.neurites]) + ref_segs_origin = np.concatenate([neurite.segment_radial_distances(s, origin) + for s in NEURON.neurites]) - rad_dists = get_feature('segment_radial_distances', NEURON) - rad_dists_origin = get_feature('segment_radial_distances', NEURON, origin=origin) + rad_dists = features.get('segment_radial_distances', NEURON) + rad_dists_origin = features.get('segment_radial_distances', NEURON, origin=origin) assert np.all(rad_dists == ref_segs) assert np.all(rad_dists_origin == ref_segs_origin) assert np.all(rad_dists_origin != ref_segs) - nrns = [nm.load_neuron(Path(SWC_PATH, f)) for + morphs = [nm.load_morphology(Path(SWC_PATH, f)) for f in ('point_soma_single_neurite.swc', 'point_soma_single_neurite2.swc')] - pop = Population(nrns) - rad_dist_nrns = [] - for nrn in nrns: - rad_dist_nrns.extend(nm.get('segment_radial_distances', nrn)) + pop = Population(morphs) + rad_dist_morphs = [] + for m in morphs: + rad_dist_morphs.extend(features.get('segment_radial_distances', m)) - rad_dist_nrns = np.array(rad_dist_nrns) - rad_dist_pop = nm.get('segment_radial_distances', pop) - assert_allclose(rad_dist_nrns, rad_dist_pop) + rad_dist_morphs = np.array(rad_dist_morphs) + rad_dist_pop = features.get('segment_radial_distances', pop) + assert_allclose(rad_dist_morphs, rad_dist_pop) def test_section_radial_distances_endpoint(): - ref_sec_rad_dist = nf.section_radial_distances(NEURON) - - rad_dists = get_feature('section_radial_distances', NEURON) + ref_sec_rad_dist = np.concatenate([neurite.section_radial_distances(s) + for s in NEURON.neurites]) + rad_dists = features.get('section_radial_distances', NEURON) assert len(rad_dists) == 84 assert np.all(rad_dists == ref_sec_rad_dist) - nrns = [nm.load_neuron(Path(SWC_PATH, f)) for + morphs = [nm.load_morphology(Path(SWC_PATH, f)) for f in ('point_soma_single_neurite.swc', 'point_soma_single_neurite2.swc')] - pop = Population(nrns) - rad_dist_nrns = [nm.get('section_radial_distances', nrn) for nrn in nrns] - rad_dist_pop = nm.get('section_radial_distances', pop) - assert_items_equal(rad_dist_pop, rad_dist_nrns) + pop = Population(morphs) + rad_dist_morphs = [v for m in morphs for v in features.get('section_radial_distances', m)] + rad_dist_pop = features.get('section_radial_distances', pop) + assert_allclose(rad_dist_pop, rad_dist_morphs) + + rad_dists = features.get('section_radial_distances', NEURON, neurite_type=NeuriteType.axon) + assert len(rad_dists) == 21 def test_section_radial_distances_origin(): origin = (-100, -200, -300) - ref_sec_rad_dist_origin = nf.section_radial_distances(NEURON, origin=origin) - rad_dists = get_feature('section_radial_distances', NEURON, origin=origin) + ref_sec_rad_dist_origin = np.concatenate([neurite.section_radial_distances(s, origin) + for s in NEURON.neurites]) + rad_dists = features.get('section_radial_distances', NEURON, origin=origin) assert len(rad_dists) == 84 assert np.all(rad_dists == ref_sec_rad_dist_origin) -def test_section_radial_axon(): - rad_dists = get_feature('section_radial_distances', NEURON, neurite_type=NeuriteType.axon) - assert len(rad_dists) == 21 - - -def test_number_of_sections_all(): - assert get_feature('number_of_sections', NEURON)[0] == 84 - assert get_feature('number_of_sections', NEURON, neurite_type=NeuriteType.all)[0] == 84 - - -def test_number_of_sections_axon(): - assert get_feature('number_of_sections', NEURON, neurite_type=NeuriteType.axon)[0] == 21 - - -def test_number_of_sections_basal(): - assert get_feature('number_of_sections', NEURON, neurite_type=NeuriteType.basal_dendrite)[0] == 42 - - -def test_n_sections_apical(): - assert get_feature('number_of_sections', NEURON, neurite_type=NeuriteType.apical_dendrite)[0] == 21 - - -def test_section_number_invalid(): - assert get_feature('number_of_sections', NEURON, neurite_type=NeuriteType.soma)[0] == 0 - assert get_feature('number_of_sections', NEURON, neurite_type=NeuriteType.undefined)[0] == 0 - - -def test_per_neurite_number_of_sections(): - nsecs = get_feature('number_of_sections_per_neurite', NEURON) +def test_number_of_sections_per_neurite(): + nsecs = features.get('number_of_sections_per_neurite', NEURON) assert len(nsecs) == 4 assert np.all(nsecs == [21, 21, 21, 21]) - -def test_per_neurite_number_of_sections_axon(): - nsecs = get_feature('number_of_sections_per_neurite', NEURON, neurite_type=NeuriteType.axon) + nsecs = features.get('number_of_sections_per_neurite', NEURON, neurite_type=NeuriteType.axon) assert len(nsecs) == 1 assert nsecs == [21] - -def test_n_sections_per_neurite_basal(): - nsecs = get_feature('number_of_sections_per_neurite', NEURON, - neurite_type=NeuriteType.basal_dendrite) + nsecs = features.get('number_of_sections_per_neurite', NEURON, + neurite_type=NeuriteType.basal_dendrite) assert len(nsecs) == 2 assert np.all(nsecs == [21, 21]) - -def test_n_sections_per_neurite_apical(): - nsecs = get_feature('number_of_sections_per_neurite', NEURON, - neurite_type=NeuriteType.apical_dendrite) + nsecs = features.get('number_of_sections_per_neurite', NEURON, + neurite_type=NeuriteType.apical_dendrite) assert len(nsecs) == 1 assert np.all(nsecs == [21]) -def test_neurite_number(): - assert get_feature('number_of_neurites', NEURON)[0] == 4 - assert get_feature('number_of_neurites', NEURON, neurite_type=NeuriteType.all)[0] == 4 - assert get_feature('number_of_neurites', NEURON, neurite_type=NeuriteType.axon)[0] == 1 - assert get_feature('number_of_neurites', NEURON, neurite_type=NeuriteType.basal_dendrite)[0] == 2 - assert get_feature('number_of_neurites', NEURON, neurite_type=NeuriteType.apical_dendrite)[0] == 1 - assert get_feature('number_of_neurites', NEURON, neurite_type=NeuriteType.soma)[0] == 0 - assert get_feature('number_of_neurites', NEURON, neurite_type=NeuriteType.undefined)[0] == 0 - - def test_trunk_origin_radii(): - assert_allclose(get_feature('trunk_origin_radii', NEURON), - [0.85351288499400002, - 0.18391483031299999, - 0.66943255462899998, - 0.14656092843999999]) - - assert_allclose(get_feature('trunk_origin_radii', NEURON, neurite_type=NeuriteType.apical_dendrite), - [0.14656092843999999]) - assert_allclose(get_feature('trunk_origin_radii', NEURON, neurite_type=NeuriteType.basal_dendrite), - [0.18391483031299999, - 0.66943255462899998]) - assert_allclose(get_feature('trunk_origin_radii', NEURON, neurite_type=NeuriteType.axon), - [0.85351288499400002]) - - -def test_get_trunk_section_lengths(): - assert_allclose(get_feature('trunk_section_lengths', NEURON), - [9.579117366740002, - 7.972322416776259, - 8.2245287740603779, - 9.212707985134525]) - assert_allclose(get_feature('trunk_section_lengths', NEURON, neurite_type=NeuriteType.apical_dendrite), - [9.212707985134525]) - assert_allclose(get_feature('trunk_section_lengths', NEURON, neurite_type=NeuriteType.basal_dendrite), - [7.972322416776259, 8.2245287740603779]) - assert_allclose(get_feature('trunk_section_lengths', NEURON, neurite_type=NeuriteType.axon), - [9.579117366740002]) - - -def test_soma_radii(): - assert_allclose(get_feature('soma_radii', NEURON)[0], 0.13065629648763766) - - -def test_soma_surface_areas(): - area = 4. * math.pi * get_feature('soma_radii', NEURON)[0] ** 2 - assert_allclose(get_feature('soma_surface_areas', NEURON), area) + assert_allclose( + features.get('trunk_origin_radii', NEURON), + [0.85351288499400002, 0.18391483031299999, 0.66943255462899998, 0.14656092843999999]) + assert_allclose( + features.get('trunk_origin_radii', NEURON, neurite_type=NeuriteType.apical_dendrite), + [0.14656092843999999]) + assert_allclose( + features.get('trunk_origin_radii', NEURON, neurite_type=NeuriteType.basal_dendrite), + [0.18391483031299999, 0.66943255462899998]) + assert_allclose( + features.get('trunk_origin_radii', NEURON, neurite_type=NeuriteType.axon), + [0.85351288499400002]) + + +def test_trunk_section_lengths(): + assert_allclose( + features.get('trunk_section_lengths', NEURON), + [9.579117366740002, 7.972322416776259, 8.2245287740603779, 9.212707985134525]) + assert_allclose( + features.get('trunk_section_lengths', NEURON, neurite_type=NeuriteType.apical_dendrite), + [9.212707985134525]) + assert_allclose( + features.get('trunk_section_lengths', NEURON, neurite_type=NeuriteType.basal_dendrite), + [7.972322416776259, 8.2245287740603779]) + assert_allclose( + features.get('trunk_section_lengths', NEURON, neurite_type=NeuriteType.axon), + [9.579117366740002]) + + +def test_soma_radius(): + assert_allclose(features.get('soma_radius', NEURON), 0.13065629648763766) + + +def test_soma_surface_area(): + area = 4. * math.pi * features.get('soma_radius', NEURON) ** 2 + assert_allclose(features.get('soma_surface_area', NEURON), area) def test_sholl_frequency(): - assert_allclose(get_feature('sholl_frequency', NEURON), + assert_allclose(features.get('sholl_frequency', NEURON), [4, 8, 8, 14, 9, 8, 7, 7, 7, 5]) - assert_allclose(get_feature('sholl_frequency', NEURON, neurite_type=NeuriteType.all), + assert_allclose(features.get('sholl_frequency', NEURON, neurite_type=NeuriteType.all), [4, 8, 8, 14, 9, 8, 7, 7, 7, 5]) - assert_allclose(get_feature('sholl_frequency', NEURON, neurite_type=NeuriteType.apical_dendrite), - [1, 2, 2, 2, 2, 2, 1, 1, 3, 3]) + assert_allclose( + features.get('sholl_frequency', NEURON, neurite_type=NeuriteType.apical_dendrite), + [1, 2, 2, 2, 2, 2, 1, 1, 3, 3]) - assert_allclose(get_feature('sholl_frequency', NEURON, neurite_type=NeuriteType.basal_dendrite), - [2, 4, 4, 6, 5, 4, 4, 4, 2, 2]) + assert_allclose( + features.get('sholl_frequency', NEURON, neurite_type=NeuriteType.basal_dendrite), + [2, 4, 4, 6, 5, 4, 4, 4, 2, 2]) - assert_allclose(get_feature('sholl_frequency', NEURON, neurite_type=NeuriteType.axon), + assert_allclose(features.get('sholl_frequency', NEURON, neurite_type=NeuriteType.axon), [1, 2, 2, 6, 2, 2, 2, 2, 2]) - assert len(get_feature('sholl_frequency', POP)) == 108 - - -@pytest.mark.skip('test_get_segment_lengths is disabled in test_get_features') -def test_section_path_distances_endpoint(): - - ref_sec_path_len_start = list(iter_neurites(NEURON, sec.start_point_path_length)) - ref_sec_path_len = list(iter_neurites(NEURON, sec.end_point_path_length)) - path_lengths = get_feature('section_path_distances', NEURON) - assert ref_sec_path_len != ref_sec_path_len_start - assert len(path_lengths) == 84 - assert np.all(path_lengths == ref_sec_path_len) + assert len(features.get('sholl_frequency', POP)) == 108 -@pytest.mark.skip('test_get_segment_lengths is disabled in test_get_features') -def test_section_path_distances_start_point(): - ref_sec_path_len_start = list(iter_neurites(NEURON, sec.start_point_path_length)) - path_lengths = get_feature('section_path_distances', NEURON, use_start_point=True) - assert len(path_lengths) == 84 - assert np.all(path_lengths == ref_sec_path_len_start) - - -def test_partition(): - assert np.all(get_feature('partition', POP)[:10] == np.array( - [19., 17., 15., 13., 11., 9., 7., 5., 3., 1.])) +def test_bifurcation_partitions(): + assert_allclose(features.get('bifurcation_partitions', POP)[:10], + [19., 17., 15., 13., 11., 9., 7., 5., 3., 1.]) def test_partition_asymmetry(): - assert_allclose(get_feature('partition_asymmetry', POP)[:10], np.array([0.9, 0.88888889, 0.875, - 0.85714286, 0.83333333, - - 0.8, 0.75, 0.66666667, - 0.5, 0.])) + assert_allclose( + features.get('partition_asymmetry', POP)[:10], + [0.9, 0.88888889, 0.875, 0.85714286, 0.83333333, 0.8, 0.75, 0.66666667, 0.5, 0.]) def test_partition_asymmetry_length(): - assert_allclose(get_feature('partition_asymmetry_length', POP)[:1], np.array([0.853925])) + assert_allclose(features.get('partition_asymmetry_length', POP)[:1], np.array([0.853925])) def test_section_strahler_orders(): path = Path(SWC_PATH, 'strahler.swc') - n = nm.load_neuron(path) - assert_allclose(get_feature('section_strahler_orders', n), + n = nm.load_morphology(path) + assert_allclose(features.get('section_strahler_orders', n), [4, 1, 4, 3, 2, 1, 1, 2, 1, 1, 3, 1, 3, 2, 1, 1, 2, 1, 1]) + + +def test_section_bif_radial_distances(): + trm_rads = features.get('section_bif_radial_distances', NRN, neurite_type=nm.AXON) + assert_allclose(trm_rads, + [8.842008561870646, + 16.7440421479104, + 23.070306480850533, + 30.181121708042546, + 36.62766031035137, + 43.967487830324885, + 51.91971040624528, + 59.427722328770955, + 66.25222507299583, + 74.05119754074926]) + + +def test_section_term_radial_distances(): + trm_rads = features.get('section_term_radial_distances', NRN, neurite_type=nm.APICAL_DENDRITE) + assert_allclose(trm_rads, + [16.22099879395879, + 25.992977561564082, + 33.31600613822663, + 42.721314797308175, + 52.379508081911546, + 59.44327819128149, + 67.07832724133213, + 79.97743930553612, + 87.10434825508366, + 97.25246040544428, + 99.58945832481642]) + + +def test_principal_direction_extents(): + m = nm.load_morphology(SWC_PATH / 'simple.swc') + principal_dir = features.get('principal_direction_extents', m) + assert_allclose(principal_dir, [14.736052694538641, 12.105102672688004]) + + # test with a realistic morphology + m = nm.load_morphology(DATA_PATH / 'h5/v1' / 'bio_neuron-000.h5') + p_ref = [1672.9694359427331, 142.43704397865031, 226.45895382204986, + 415.50612748523838, 429.83008974193206, 165.95410536922873, + 346.83281498399697] + p = features.get('principal_direction_extents', m) + assert_allclose(p, p_ref, rtol=1e-6) diff --git a/tests/features/test_morphology.py b/tests/features/test_morphology.py new file mode 100644 index 000000000..966a114f6 --- /dev/null +++ b/tests/features/test_morphology.py @@ -0,0 +1,280 @@ +# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Test ``features.morphology``.""" +from math import pi, sqrt +import tempfile +import warnings +from io import StringIO +from pathlib import Path + +import numpy as np +from neurom import NeuriteType, load_morphology, AXON, BASAL_DENDRITE +from neurom.features import morphology, section + +import pytest +from numpy.testing import assert_almost_equal, assert_array_almost_equal, assert_array_equal, assert_allclose + +DATA_PATH = Path(__file__).parent.parent / 'data' +H5_PATH = DATA_PATH / 'h5/v1' +NRN = load_morphology(H5_PATH / 'Neuron.h5') +SWC_PATH = DATA_PATH / 'swc' +SIMPLE = load_morphology(SWC_PATH / 'simple.swc') +SIMPLE_TRUNK = load_morphology(SWC_PATH / 'simple_trunk.swc') +SWC_NRN = load_morphology(SWC_PATH / 'Neuron.swc') +with warnings.catch_warnings(record=True): + SWC_NRN_3PT = load_morphology(SWC_PATH / 'soma' / 'three_pt_soma.swc') + + +def test_soma_volume(): + with warnings.catch_warnings(record=True): + # SomaSinglePoint + ret = morphology.soma_volume(SIMPLE) + assert_almost_equal(ret, 4.1887902047863905) + # SomaCylinders + ret = morphology.soma_volume(SWC_NRN) + assert_almost_equal(ret, 0.010726068245337955) + # SomaSimpleContour + ret = morphology.soma_volume(NRN) + assert_almost_equal(ret, 0.0033147000251481135) + # SomaNeuromorphoThreePointCylinders + ret = morphology.soma_volume(SWC_NRN_3PT) + assert_almost_equal(ret, 50.26548245743669) + + +def test_soma_surface_area(): + assert_allclose(morphology.soma_surface_area(SIMPLE), 12.566370614359172) + assert_allclose(morphology.soma_surface_area(NRN), 0.1075095256160432) + + +def test_soma_radius(): + assert morphology.soma_radius(SIMPLE) == 1 + assert_allclose(morphology.soma_radius(NRN), 0.09249506049313666) + + +def test_total_area_per_neurite(): + def surface(r0, r1, h): + return pi * (r0 + r1) * sqrt((r0 - r1) ** 2 + h ** 2) + + basal_area = surface(1, 1, 5) + surface(1, 0, 5) + surface(1, 0, 6) + ret = morphology.total_area_per_neurite(SIMPLE, neurite_type=BASAL_DENDRITE) + assert_almost_equal(ret[0], basal_area) + + axon_area = surface(1, 1, 4) + surface(1, 0, 5) + surface(1, 0, 6) + ret = morphology.total_area_per_neurite(SIMPLE, neurite_type=AXON) + assert_almost_equal(ret[0], axon_area) + + ret = morphology.total_area_per_neurite(SIMPLE) + assert np.allclose(ret, [basal_area, axon_area]) + + +def test_total_volume_per_neurite(): + vol = morphology.total_volume_per_neurite(NRN) + assert len(vol) == 4 + + # calculate the volumes by hand and compare + vol2 = [sum(section.section_volume(s) for s in n.iter_sections()) + for n in NRN.neurites] + assert vol == vol2 + + # regression test + ref_vol = [271.94122143951864, 281.24754646913954, + 274.98039928781355, 276.73860261723024] + assert np.allclose(vol, ref_vol) + + +def test_total_length_per_neurite(): + total_lengths = morphology.total_length_per_neurite(SIMPLE) + assert total_lengths == [5. + 5. + 6., 4. + 5. + 6.] + + +def test_number_of_neurites(): + assert morphology.number_of_neurites(SIMPLE) == 2 + + +def test_total_volume_per_neurite(): + # note: cannot use SIMPLE since it lies in a plane + total_volumes = morphology.total_volume_per_neurite(NRN) + assert_allclose(total_volumes, + [271.94122143951864, 281.24754646913954, 274.98039928781355, 276.73860261723024]) + + +def test_number_of_sections_per_neurite(): + sections = morphology.number_of_sections_per_neurite(SIMPLE) + assert_allclose(sections, (3, 3)) + + +def test_trunk_section_lengths(): + ret = morphology.trunk_section_lengths(SIMPLE) + assert ret == [5.0, 4.0] + + +def test_trunk_origin_radii(): + ret = morphology.trunk_origin_radii(SIMPLE) + assert ret == [1.0, 1.0] + + +def test_trunk_origin_azimuths(): + ret = morphology.trunk_origin_azimuths(SIMPLE) + assert ret == [0.0, 0.0] + + +def test_trunk_angles(): + ret = morphology.trunk_angles(SIMPLE_TRUNK) + assert_array_almost_equal(ret, [np.pi/2, np.pi/2, np.pi/2, np.pi/2]) + ret = morphology.trunk_angles(SIMPLE_TRUNK, neurite_type=NeuriteType.basal_dendrite) + assert_array_almost_equal(ret, [np.pi, np.pi]) + ret = morphology.trunk_angles(SIMPLE_TRUNK, neurite_type=NeuriteType.axon) + assert_array_almost_equal(ret, [0.0]) + ret = morphology.trunk_angles(SIMPLE, neurite_type=NeuriteType.apical_dendrite) + assert_array_almost_equal(ret, []) + + +def test_trunk_vectors(): + ret = morphology.trunk_vectors(SIMPLE_TRUNK) + assert_array_equal(ret[0], [0., -1., 0.]) + assert_array_equal(ret[1], [1., 0., 0.]) + assert_array_equal(ret[2], [-1., 0., 0.]) + assert_array_equal(ret[3], [0., 1., 0.]) + ret = morphology.trunk_vectors(SIMPLE_TRUNK, neurite_type=NeuriteType.axon) + assert_array_equal(ret[0], [0., -1., 0.]) + + +def test_trunk_origin_elevations(): + n0 = load_morphology(StringIO(u""" + 1 1 0 0 0 4 -1 + 2 3 1 0 0 2 1 + 3 3 2 1 1 2 2 + 4 3 0 1 0 2 1 + 5 3 1 2 1 2 4 + """), reader='swc') + + n1 = load_morphology(StringIO(u""" + 1 1 0 0 0 4 -1 + 2 3 0 -1 0 2 1 + 3 3 -1 -2 -1 2 2 + """), reader='swc') + + pop = [n0, n1] + assert_allclose(morphology.trunk_origin_elevations(n0), [0.0, np.pi / 2.]) + assert_allclose(morphology.trunk_origin_elevations(n1), [-np.pi / 2.]) + assert_allclose(morphology.trunk_origin_elevations(n0, NeuriteType.basal_dendrite), [0.0, np.pi / 2.]) + assert_allclose(morphology.trunk_origin_elevations(n1, NeuriteType.basal_dendrite), [-np.pi / 2.]) + + assert morphology.trunk_origin_elevations(n0, NeuriteType.axon) == [] + assert morphology.trunk_origin_elevations(n1, NeuriteType.axon) == [] + assert morphology.trunk_origin_elevations(n0, NeuriteType.apical_dendrite) == [] + assert morphology.trunk_origin_elevations(n1, NeuriteType.apical_dendrite) == [] + + +def test_trunk_elevation_zero_norm_vector_raises(): + with pytest.raises(Exception): + morphology.trunk_origin_elevations(SWC_NRN) + + +def test_sholl_crossings_simple(): + center = SIMPLE.soma.center + radii = [] + assert (list(morphology.sholl_crossings(SIMPLE, center, radii=radii)) == []) + assert (list(morphology.sholl_crossings(SIMPLE, radii=radii)) == []) + assert (list(morphology.sholl_crossings(SIMPLE)) == [2]) + + radii = [1.0] + assert ([2] == + list(morphology.sholl_crossings(SIMPLE, center, radii=radii))) + + radii = [1.0, 5.1] + assert ([2, 4] == + list(morphology.sholl_crossings(SIMPLE, center, radii=radii))) + + radii = [1., 4., 5.] + assert ([2, 4, 5] == + list(morphology.sholl_crossings(SIMPLE, center, radii=radii))) + + assert ([1, 1, 2] == + list(morphology.sholl_crossings(SIMPLE.sections[:2], center, radii=radii))) + + +def load_swc(string): + with tempfile.NamedTemporaryFile(prefix='test_morphology', mode='w', suffix='.swc') as fd: + fd.write(string) + fd.flush() + return load_morphology(fd.name) + + +def test_sholl_analysis_custom(): + # recreate morphs from Fig 2 of + # http://dx.doi.org/10.1016/j.jneumeth.2014.01.016 + radii = np.arange(10, 81, 10) + center = 0, 0, 0 + morph_A = load_swc("""\ + 1 1 0 0 0 1. -1 + 2 3 0 0 0 1. 1 + 3 3 80 0 0 1. 2 + 4 4 0 0 0 1. 1 + 5 4 -80 0 0 1. 4""") + assert (list(morphology.sholl_crossings(morph_A, center, radii=radii)) == + [2, 2, 2, 2, 2, 2, 2, 2]) + + morph_B = load_swc("""\ + 1 1 0 0 0 1. -1 + 2 3 0 0 0 1. 1 + 3 3 35 0 0 1. 2 + 4 3 51 10 0 1. 3 + 5 3 51 5 0 1. 3 + 6 3 51 0 0 1. 3 + 7 3 51 -5 0 1. 3 + 8 3 51 -10 0 1. 3 + 9 4 -35 0 0 1. 2 +10 4 -51 10 0 1. 9 +11 4 -51 5 0 1. 9 +12 4 -51 0 0 1. 9 +13 4 -51 -5 0 1. 9 +14 4 -51 -10 0 1. 9 + """) + assert (list(morphology.sholl_crossings(morph_B, center, radii=radii)) == + [2, 2, 2, 10, 10, 0, 0, 0]) + + morph_C = load_swc("""\ + 1 1 0 0 0 1. -1 + 2 3 0 0 0 1. 1 + 3 3 65 0 0 1. 2 + 4 3 85 10 0 1. 3 + 5 3 85 5 0 1. 3 + 6 3 85 0 0 1. 3 + 7 3 85 -5 0 1. 3 + 8 3 85 -10 0 1. 3 + 9 4 65 0 0 1. 2 +10 4 85 10 0 1. 9 +11 4 85 5 0 1. 9 +12 4 85 0 0 1. 9 +13 4 85 -5 0 1. 9 +14 4 85 -10 0 1. 9 + """) + assert (list(morphology.sholl_crossings(morph_C, center, radii=radii)) == + [2, 2, 2, 2, 2, 2, 10, 10]) diff --git a/tests/features/test_neurite.py b/tests/features/test_neurite.py new file mode 100644 index 000000000..07d9422bf --- /dev/null +++ b/tests/features/test_neurite.py @@ -0,0 +1,244 @@ +# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# All rights reserved. +# +# This file is part of NeuroM +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of +# its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Test features.neuritefunc.""" + +from math import pi, sqrt +from pathlib import Path + +import neurom as nm +import numpy as np +import scipy +from mock import patch +from neurom.features import neurite, morphology +from neurom.geom import convex_hull + +import pytest +from numpy.testing import assert_allclose + +DATA_PATH = Path(__file__).parent.parent / 'data' +H5_PATH = DATA_PATH / 'h5/v1' +SWC_PATH = DATA_PATH / 'swc' +SIMPLE = nm.load_morphology(SWC_PATH / 'simple.swc') +NRN = nm.load_morphology(H5_PATH / 'Neuron.h5') + + +def test_number_of_bifurcations(): + assert neurite.number_of_bifurcations(SIMPLE.neurites[0]) == 1 + assert neurite.number_of_bifurcations(SIMPLE.neurites[1]) == 1 + + +def test_number_of_forking_points(): + assert neurite.number_of_forking_points(SIMPLE.neurites[0]) == 1 + assert neurite.number_of_forking_points(SIMPLE.neurites[1]) == 1 + + +def test_number_of_leaves(): + assert neurite.number_of_leaves(SIMPLE.neurites[0]) == 2 + assert neurite.number_of_leaves(SIMPLE.neurites[1]) == 2 + + +def test_neurite_volume_density(): + vol = np.array(morphology.total_volume_per_neurite(NRN)) + hull_vol = np.array([convex_hull(n).volume for n in nm.iter_neurites(NRN)]) + + vol_density = [neurite.volume_density(s) for s in NRN.neurites] + assert len(vol_density) == 4 + assert np.allclose(vol_density, vol / hull_vol) + + ref_density = [0.43756606998299519, 0.52464681266899216, + 0.24068543213643726, 0.26289304906104355] + assert_allclose(vol_density, ref_density) + + +def test_neurite_volume_density_failed_convex_hull(): + with patch('neurom.features.neurite.convex_hull', + side_effect=scipy.spatial.qhull.QhullError('boom')): + vol_density = neurite.volume_density(NRN) + assert vol_density, np.nan + + +def test_terminal_path_length_per_neurite(): + terminal_distances = [neurite.terminal_path_lengths(s) for s in SIMPLE.neurites] + assert terminal_distances == [[10, 11], [10, 9]] + + +def test_max_radial_distance(): + assert_allclose([neurite.max_radial_distance(s) for s in SIMPLE.neurites], + [7.81025, 7.2111025]) + + +def test_number_of_segments(): + assert [neurite.number_of_segments(s) for s in SIMPLE.neurites] == [3, 3] + + +def test_number_of_sections(): + assert [neurite.number_of_sections(s) for s in SIMPLE.neurites] == [3, 3] + + +def test_section_path_distances(): + path_lengths = [neurite.section_path_distances(s) for s in SIMPLE.neurites] + assert path_lengths == [[5., 10., 11.], [4., 10., 9.]] + + +def test_section_term_lengths(): + term_lengths = [neurite.section_term_lengths(s) for s in SIMPLE.neurites] + assert term_lengths == [[5., 6.], [6., 5.]] + + +def test_section_bif_lengths(): + bif_lengths = [neurite.section_bif_lengths(s) for s in SIMPLE.neurites] + assert bif_lengths == [[5.], [4.]] + + +def test_section_end_distances(): + end_dist = [neurite.section_end_distances(s) for s in SIMPLE.neurites] + assert end_dist == [[5.0, 5.0, 6.0], [4.0, 6.0, 5.0]] + + +def test_section_partition_pairs(): + part_pairs = [neurite.partition_pairs(s) for s in SIMPLE.neurites] + assert part_pairs == [[(1.0, 1.0)], [(1.0, 1.0)]] + + +def test_section_bif_radial_distances(): + bif_rads = [neurite.section_bif_radial_distances(s) for s in SIMPLE.neurites] + assert bif_rads == [[5.], [4.]] + + +def test_section_term_radial_distances(): + trm_rads = [neurite.section_term_radial_distances(s) for s in SIMPLE.neurites] + assert_allclose(trm_rads, [[7.0710678118654755, 7.810249675906654], [7.211102550927978, 6.4031242374328485]]) + + +def test_section_branch_orders(): + branch_orders = [neurite.section_branch_orders(s) for s in SIMPLE.neurites] + assert_allclose(branch_orders, [[0, 1, 1], [0, 1, 1]]) + + +def test_section_bif_branch_orders(): + bif_branch_orders = [neurite.section_bif_branch_orders(s) for s in SIMPLE.neurites] + assert bif_branch_orders == [[0], [0]] + + +def test_section_term_branch_orders(): + term_branch_orders = [neurite.section_term_branch_orders(s) for s in SIMPLE.neurites] + assert term_branch_orders == [[1, 1], [1, 1]] + + +def test_section_radial_distances(): + radial_distances = [neurite.section_radial_distances(s) for s in SIMPLE.neurites] + assert_allclose(radial_distances, + [[5.0, sqrt(5**2 + 5**2), sqrt(6**2 + 5**2)], + [4.0, sqrt(6**2 + 4**2), sqrt(5**2 + 4**2)]]) + + +def test_local_bifurcation_angles(): + local_bif_angles = [neurite.local_bifurcation_angles(s) for s in SIMPLE.neurites] + assert_allclose(local_bif_angles, [[pi], [pi]]) + + +def test_remote_bifurcation_angles(): + remote_bif_angles = [neurite.remote_bifurcation_angles(s) for s in SIMPLE.neurites] + assert_allclose(remote_bif_angles, [[pi], [pi]]) + + +def test_partition(): + partition = [neurite.bifurcation_partitions(s) for s in SIMPLE.neurites] + assert_allclose(partition, [[1.0], [1.0]]) + + +def test_partition_asymmetry(): + partition = [neurite.partition_asymmetry(s) for s in SIMPLE.neurites] + assert_allclose(partition, [[0.0], [0.0]]) + + partition = [neurite.partition_asymmetry(s, variant='length') for s in SIMPLE.neurites] + assert_allclose(partition, [[0.0625], [0.06666666666666667]]) + + with pytest.raises(ValueError): + neurite.partition_asymmetry(SIMPLE, variant='invalid-variant') + + with pytest.raises(ValueError): + neurite.partition_asymmetry(SIMPLE, method='invalid-method') + + +def test_segment_lengths(): + segment_lengths = [neurite.segment_lengths(s) for s in SIMPLE.neurites] + assert_allclose(segment_lengths, [[5.0, 5.0, 6.0], [4.0, 6.0, 5.0]]) + + +def test_segment_areas(): + result = [neurite.segment_areas(s) for s in SIMPLE.neurites] + assert_allclose(result, [[31.415927, 16.019042, 19.109562], [25.132741, 19.109562, 16.019042]]) + + +def test_segment_volumes(): + expected = [[15.70796327, 5.23598776, 6.28318531], [12.56637061, 6.28318531, 5.23598776]] + result = [neurite.segment_volumes(s) for s in SIMPLE.neurites] + assert_allclose(result, expected) + + +def test_segment_midpoints(): + midpoints = [neurite.segment_midpoints(s) for s in SIMPLE.neurites] + assert_allclose(midpoints, + [[[0., (5. + 0) / 2, 0.], # trunk type 2 + [-2.5, 5., 0.], + [3., 5., 0.]], + [[0., (-4. + 0) / 2., 0.], # trunk type 3 + [3., -4., 0.], + [-2.5, -4., 0.]]]) + + +def test_segment_radial_distances(): + """midpoints on segments.""" + radial_distances = [neurite.segment_radial_distances(s) for s in SIMPLE.neurites] + assert_allclose(radial_distances, + [[2.5, sqrt(2.5**2 + 5**2), sqrt(3**2 + 5**2)], [2.0, 5.0, sqrt(2.5**2 + 4**2)]]) + + +def test_segment_path_lengths(): + pathlengths = [neurite.segment_path_lengths(s) for s in SIMPLE.neurites] + assert_allclose(pathlengths, [[5., 10., 11.], [4., 10., 9.]]) + + pathlengths = neurite.segment_path_lengths(NRN.neurites[0])[:5] + assert_allclose(pathlengths, [0.1, 1.332525, 2.5301487, 3.267878, 4.471462]) + + +def test_section_taper_rates(): + assert_allclose(neurite.section_taper_rates(NRN.neurites[0])[:10], + [0.06776235492169848, + 0.0588716599404923, + 0.03791571485186163, + 0.04674653812192691, + -0.026399800285566058, + -0.026547582897720887, + -0.045038414440432537, + 0.02083822978267914, + -0.0027721371791201038, + 0.0803069042861474], + atol=1e-4) diff --git a/tests/features/test_neuritefunc.py b/tests/features/test_neuritefunc.py deleted file mode 100644 index 1082173a9..000000000 --- a/tests/features/test_neuritefunc.py +++ /dev/null @@ -1,397 +0,0 @@ -# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Test neurom._neuritefunc functionality.""" - -from math import pi, sqrt -from pathlib import Path - -import neurom as nm -import numpy as np -import scipy -from mock import patch -from neurom.features import neuritefunc as _nf -from neurom.features import sectionfunc as sectionfunc -from neurom.geom import convex_hull - -import pytest -from numpy.testing import assert_allclose, assert_array_almost_equal, assert_almost_equal -from utils import _close - -DATA_PATH = Path(__file__).parent.parent / 'data' -H5_PATH = DATA_PATH / 'h5/v1' -SWC_PATH = DATA_PATH / 'swc' -SIMPLE = nm.load_neuron(SWC_PATH / 'simple.swc') -NRN = nm.load_neuron(H5_PATH / 'Neuron.h5') - - -def test_principal_direction_extents(): - principal_dir = list(_nf.principal_direction_extents(SIMPLE)) - assert_allclose(principal_dir, - (14.736052694538641, 12.105102672688004)) - - # test with a realistic neuron - nrn = nm.load_neuron(Path(H5_PATH, 'bio_neuron-000.h5')) - - p_ref = [1672.9694359427331, 142.43704397865031, 226.45895382204986, - 415.50612748523838, 429.83008974193206, 165.95410536922873, - 346.83281498399697] - - p = _nf.principal_direction_extents(nrn) - _close(np.array(p), np.array(p_ref)) - - -def test_n_bifurcation_points(): - assert _nf.n_bifurcation_points(SIMPLE.neurites[0]) == 1 - assert _nf.n_bifurcation_points(SIMPLE.neurites[1]) == 1 - assert _nf.n_bifurcation_points(SIMPLE.neurites) == 2 - - -def test_n_forking_points(): - assert _nf.n_forking_points(SIMPLE.neurites[0]) == 1 - assert _nf.n_forking_points(SIMPLE.neurites[1]) == 1 - assert _nf.n_forking_points(SIMPLE.neurites) == 2 - - -def test_n_leaves(): - assert _nf.n_leaves(SIMPLE.neurites[0]) == 2 - assert _nf.n_leaves(SIMPLE.neurites[1]) == 2 - assert _nf.n_leaves(SIMPLE.neurites) == 4 - - -def test_total_area_per_neurite(): - def surface(r0, r1, h): - return pi * (r0 + r1) * sqrt((r0 - r1) ** 2 + h ** 2) - - basal_area = surface(1, 1, 5) + surface(1, 0, 5) + surface(1, 0, 6) - ret = _nf.total_area_per_neurite(SIMPLE, - neurite_type=nm.BASAL_DENDRITE) - assert_almost_equal(ret[0], basal_area) - - axon_area = surface(1, 1, 4) + surface(1, 0, 5) + surface(1, 0, 6) - ret = _nf.total_area_per_neurite(SIMPLE, neurite_type=nm.AXON) - assert_almost_equal(ret[0], axon_area) - - ret = _nf.total_area_per_neurite(SIMPLE) - assert np.allclose(ret, [basal_area, axon_area]) - - -def test_total_volume_per_neurite(): - vol = _nf.total_volume_per_neurite(NRN) - assert len(vol) == 4 - - # calculate the volumes by hand and compare - vol2 = [sum(sectionfunc.section_volume(s) for s in n.iter_sections()) - for n in NRN.neurites - ] - assert vol == vol2 - - # regression test - ref_vol = [271.94122143951864, 281.24754646913954, - 274.98039928781355, 276.73860261723024] - assert np.allclose(vol, ref_vol) - - -def test_neurite_volume_density(): - - vol = np.array(_nf.total_volume_per_neurite(NRN)) - hull_vol = np.array([convex_hull(n).volume for n in nm.iter_neurites(NRN)]) - - vol_density = _nf.neurite_volume_density(NRN) - assert len(vol_density) == 4 - assert np.allclose(vol_density, vol / hull_vol) - - ref_density = [0.43756606998299519, 0.52464681266899216, - 0.24068543213643726, 0.26289304906104355] - assert_allclose(vol_density, ref_density) - - -def test_neurite_volume_density_failed_convex_hull(): - with patch('neurom.features.neuritefunc.convex_hull', - side_effect=scipy.spatial.qhull.QhullError('boom')): - vol_density = _nf.neurite_volume_density(NRN) - assert vol_density, np.nan - - -def test_terminal_path_length_per_neurite(): - terminal_distances = _nf.terminal_path_lengths_per_neurite(SIMPLE) - assert_allclose(terminal_distances, - (5 + 5., 5 + 6., 4. + 6., 4. + 5)) - terminal_distances = _nf.terminal_path_lengths_per_neurite(SIMPLE, - neurite_type=nm.AXON) - assert_allclose(terminal_distances, - (4. + 6., 4. + 5.)) - - -def test_total_length_per_neurite(): - total_lengths = _nf.total_length_per_neurite(SIMPLE) - assert_allclose(total_lengths, - (5. + 5. + 6., 4. + 5. + 6.)) - - -def test_max_radial_distance(): - dmax = _nf.max_radial_distance(SIMPLE) - assert_almost_equal(dmax, 7.81025, decimal=6) - - -def test_n_segments(): - n_segments = _nf.n_segments(SIMPLE) - assert n_segments == 6 - - -def test_n_neurites(): - n_neurites = _nf.n_neurites(SIMPLE) - assert n_neurites == 2 - - -def test_n_sections(): - n_sections = _nf.n_sections(SIMPLE) - assert n_sections == 6 - - -def test_neurite_volumes(): - # note: cannot use SIMPLE since it lies in a plane - total_volumes = _nf.total_volume_per_neurite(NRN) - assert_allclose(total_volumes, - [271.94122143951864, 281.24754646913954, - 274.98039928781355, 276.73860261723024] - ) - - -def test_section_path_lengths(): - path_lengths = list(_nf.section_path_lengths(SIMPLE)) - assert_allclose(path_lengths, - (5., 10., 11., # type 3, basal dendrite - 4., 10., 9.)) # type 2, axon - - -def test_section_term_lengths(): - term_lengths = list(_nf.section_term_lengths(SIMPLE)) - assert_allclose(term_lengths, - (5., 6., 6., 5.)) - - -def test_section_bif_lengths(): - bif_lengths = list(_nf.section_bif_lengths(SIMPLE)) - assert_allclose(bif_lengths, - (5., 4.)) - - -def test_section_end_distances(): - end_dist = list(_nf.section_end_distances(SIMPLE)) - assert_allclose(end_dist, - [5.0, 5.0, 6.0, 4.0, 6.0, 5.0]) - - -def test_section_partition_pairs(): - part_pairs = list(_nf.partition_pairs(SIMPLE)) - assert_allclose(part_pairs, - [(1.0, 1.0), (1.0, 1.0)]) - - -def test_section_bif_radial_distances(): - bif_rads = list(_nf.section_bif_radial_distances(SIMPLE)) - assert_allclose(bif_rads, - [5., 4.]) - trm_rads = list(_nf.section_bif_radial_distances(NRN, neurite_type=nm.AXON)) - assert_allclose(trm_rads, - [8.842008561870646, - 16.7440421479104, - 23.070306480850533, - 30.181121708042546, - 36.62766031035137, - 43.967487830324885, - 51.91971040624528, - 59.427722328770955, - 66.25222507299583, - 74.05119754074926]) - - -def test_section_term_radial_distances(): - trm_rads = list(_nf.section_term_radial_distances(SIMPLE)) - assert_allclose(trm_rads, - [7.0710678118654755, 7.810249675906654, 7.211102550927978, 6.4031242374328485]) - trm_rads = list(_nf.section_term_radial_distances(NRN, neurite_type=nm.APICAL_DENDRITE)) - assert_allclose(trm_rads, - [16.22099879395879, - 25.992977561564082, - 33.31600613822663, - 42.721314797308175, - 52.379508081911546, - 59.44327819128149, - 67.07832724133213, - 79.97743930553612, - 87.10434825508366, - 97.25246040544428, - 99.58945832481642]) - - -def test_number_of_sections_per_neurite(): - sections = _nf.number_of_sections_per_neurite(SIMPLE) - assert_allclose(sections, - (3, 3)) - - -def test_section_branch_orders(): - branch_orders = list(_nf.section_branch_orders(SIMPLE)) - assert_allclose(branch_orders, - (0, 1, 1, # type 3, basal dendrite - 0, 1, 1)) # type 2, axon - - -def test_section_bif_branch_orders(): - bif_branch_orders = list(_nf.section_bif_branch_orders(SIMPLE)) - assert_allclose(bif_branch_orders, - (0, # type 3, basal dendrite - 0)) # type 2, axon - - -def test_section_term_branch_orders(): - term_branch_orders = list(_nf.section_term_branch_orders(SIMPLE)) - assert_allclose(term_branch_orders, - (1, 1, # type 3, basal dendrite - 1, 1)) # type 2, axon - - -def test_section_radial_distances(): - radial_distances = _nf.section_radial_distances(SIMPLE) - assert_allclose(radial_distances, - (5.0, sqrt(5**2 + 5**2), sqrt(6**2 + 5**2), # type 3, basal dendrite - 4.0, sqrt(6**2 + 4**2), sqrt(5**2 + 4**2))) # type 2, axon - - -def test_local_bifurcation_angles(): - local_bif_angles = list(_nf.local_bifurcation_angles(SIMPLE)) - assert_allclose(local_bif_angles, - (pi, pi)) - - -def test_remote_bifurcation_angles(): - remote_bif_angles = list(_nf.remote_bifurcation_angles(SIMPLE)) - assert_allclose(remote_bif_angles, - (pi, pi)) - - -def test_partition(): - partition = list(_nf.bifurcation_partitions(SIMPLE)) - assert_allclose(partition, - (1.0, 1.0)) - - -def test_partition_asymmetry(): - partition = list(_nf.partition_asymmetries(SIMPLE)) - assert_allclose(partition, - (0.0, 0.0)) - - partition = list(_nf.partition_asymmetries(SIMPLE, variant='length')) - assert_allclose(partition, - (0.0625, 0.06666666666666667)) - - with pytest.raises(ValueError): - _nf.partition_asymmetries(SIMPLE, variant='invalid-variant') - - with pytest.raises(ValueError): - _nf.partition_asymmetries(SIMPLE, method='invalid-method') - - -def test_segment_lengths(): - segment_lengths = _nf.segment_lengths(SIMPLE) - assert_allclose(segment_lengths, - (5.0, 5.0, 6.0, # type 3, basal dendrite - 4.0, 6.0, 5.0)) # type 2, axon - - -def test_segment_areas(): - result = _nf.segment_areas(SIMPLE) - assert_allclose(result, - [31.415927, - 16.019042, - 19.109562, - 25.132741, - 19.109562, - 16.019042]) - - -def test_segment_volumes(): - expected = [ - 15.70796327, - 5.23598776, - 6.28318531, - 12.56637061, - 6.28318531, - 5.23598776, - ] - result = _nf.segment_volumes(SIMPLE) - assert_allclose(result, expected) - - -def test_segment_midpoints(): - midpoints = np.array(_nf.segment_midpoints(SIMPLE)) - assert_allclose(midpoints, - np.array([[0., (5. + 0) / 2, 0.], # trunk type 2 - [-2.5, 5., 0.], - [3., 5., 0.], - [0., (-4. + 0) / 2., 0.], # trunk type 3 - [3., -4., 0.], - [-2.5, -4., 0.]])) - - -def test_segment_radial_distances(): - """midpoints on segments.""" - radial_distances = _nf.segment_radial_distances(SIMPLE) - assert_allclose(radial_distances, - [2.5, sqrt(2.5**2 + 5**2), sqrt(3**2 + 5**2), 2.0, 5.0, sqrt(2.5**2 + 4**2)]) - - -def test_segment_path_lengths(): - pathlengths = _nf.segment_path_lengths(SIMPLE) - assert_allclose(pathlengths, [5., 10., 11., 4., 10., 9.]) - - pathlengths = _nf.segment_path_lengths(NRN)[:5] - assert_array_almost_equal(pathlengths, [0.1, 1.332525, 2.530149, 3.267878, 4.471462]) - - -def test_principal_direction_extents(): - principal_dir = list(_nf.principal_direction_extents(SIMPLE)) - assert_allclose(principal_dir, - (14.736052694538641, 12.105102672688004)) - - -def test_section_taper_rates(): - assert_allclose(list(_nf.section_taper_rates(NRN.neurites[0]))[:10], - [0.06776235492169848, - 0.0588716599404923, - 0.03791571485186163, - 0.04674653812192691, - -0.026399800285566058, - -0.026547582897720887, - -0.045038414440432537, - 0.02083822978267914, - -0.0027721371791201038, - 0.0803069042861474], - atol=1e-4) diff --git a/tests/features/test_neuronfunc.py b/tests/features/test_neuronfunc.py deleted file mode 100644 index 8070a40d7..000000000 --- a/tests/features/test_neuronfunc.py +++ /dev/null @@ -1,241 +0,0 @@ -# Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project -# All rights reserved. -# -# This file is part of NeuroM -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the names of -# its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -"""Test neurom._neuronfunc functionality.""" -import tempfile -import warnings -from io import StringIO -from pathlib import Path - -import numpy as np -from neurom import NeuriteType, load_neuron -from neurom.core.population import Population -from neurom.features import neuronfunc as _nf - -import pytest -from numpy.testing import assert_almost_equal, assert_array_almost_equal, assert_array_equal - -DATA_PATH = Path(__file__).parent.parent / 'data' -H5_PATH = DATA_PATH / 'h5/v1' -NRN = load_neuron(H5_PATH / 'Neuron.h5') -SWC_PATH = DATA_PATH / 'swc' -SIMPLE = load_neuron(SWC_PATH / 'simple.swc') -SIMPLE_TRUNK = load_neuron(SWC_PATH / 'simple_trunk.swc') -SWC_NRN = load_neuron(SWC_PATH / 'Neuron.swc') -with warnings.catch_warnings(record=True): - SWC_NRN_3PT = load_neuron(SWC_PATH / 'soma' / 'three_pt_soma.swc') - - -def test_soma_volume(): - with warnings.catch_warnings(record=True): - # SomaSinglePoint - ret = _nf.soma_volume(SIMPLE) - assert_almost_equal(ret, 4.1887902047863905) - # SomaCylinders - ret = _nf.soma_volume(SWC_NRN) - assert_almost_equal(ret, 0.010726068245337955) - # SomaSimpleContour - ret = _nf.soma_volume(NRN) - assert_almost_equal(ret, 0.0033147000251481135) - # SomaNeuromorphoThreePointCylinders - ret = _nf.soma_volume(SWC_NRN_3PT) - assert_almost_equal(ret, 50.26548245743669) - - -def test_soma_volumes(): - with warnings.catch_warnings(record=True): - ret = _nf.soma_volumes(SIMPLE) - assert ret == [4.1887902047863905, ] - - -def test_soma_surface_area(): - ret = _nf.soma_surface_area(SIMPLE) - assert ret == 12.566370614359172 - - -def test_soma_surface_areas(): - ret = _nf.soma_surface_areas(SIMPLE) - assert ret == [12.566370614359172, ] - - -def test_soma_radii(): - ret = _nf.soma_radii(SIMPLE) - assert ret == [1., ] - - -def test_trunk_section_lengths(): - ret = _nf.trunk_section_lengths(SIMPLE) - assert ret == [5.0, 4.0] - - -def test_trunk_origin_radii(): - ret = _nf.trunk_origin_radii(SIMPLE) - assert ret == [1.0, 1.0] - - -def test_trunk_origin_azimuths(): - ret = _nf.trunk_origin_azimuths(SIMPLE) - assert ret == [0.0, 0.0] - - -def test_trunk_angles(): - ret = _nf.trunk_angles(SIMPLE_TRUNK) - assert_array_almost_equal(ret, [np.pi/2, np.pi/2, np.pi/2, np.pi/2]) - ret = _nf.trunk_angles(SIMPLE_TRUNK, neurite_type=NeuriteType.basal_dendrite) - assert_array_almost_equal(ret, [np.pi, np.pi]) - ret = _nf.trunk_angles(SIMPLE_TRUNK, neurite_type=NeuriteType.axon) - assert_array_almost_equal(ret, [0.0]) - ret = _nf.trunk_angles(SIMPLE, neurite_type=NeuriteType.apical_dendrite) - assert_array_almost_equal(ret, []) - - -def test_trunk_vectors(): - ret = _nf.trunk_vectors(SIMPLE_TRUNK) - assert_array_equal(ret[0], [0., -1., 0.]) - assert_array_equal(ret[1], [1., 0., 0.]) - assert_array_equal(ret[2], [-1., 0., 0.]) - assert_array_equal(ret[3], [0., 1., 0.]) - ret = _nf.trunk_vectors(SIMPLE_TRUNK, neurite_type=NeuriteType.axon) - assert_array_equal(ret[0], [0., -1., 0.]) - - -def test_trunk_origin_elevations(): - n0 = load_neuron(StringIO(u""" - 1 1 0 0 0 4 -1 - 2 3 1 0 0 2 1 - 3 3 2 1 1 2 2 - 4 3 0 1 0 2 1 - 5 3 1 2 1 2 4 - """), reader='swc') - - n1 = load_neuron(StringIO(u""" - 1 1 0 0 0 4 -1 - 2 3 0 -1 0 2 1 - 3 3 -1 -2 -1 2 2 - """), reader='swc') - - pop = Population([n0, n1]) - assert_array_almost_equal(_nf.trunk_origin_elevations(pop), - [0.0, np.pi/2., -np.pi/2.]) - - assert_array_almost_equal(_nf.trunk_origin_elevations(pop, neurite_type=NeuriteType.basal_dendrite), - [0.0, np.pi/2., -np.pi/2.]) - - assert_array_almost_equal(_nf.trunk_origin_elevations(pop, neurite_type=NeuriteType.axon), - []) - - assert_array_almost_equal(_nf.trunk_origin_elevations(pop, neurite_type=NeuriteType.apical_dendrite), - []) - - -def test_trunk_elevation_zero_norm_vector_raises(): - with pytest.raises(Exception): - _nf.trunk_origin_elevations(SWC_NRN) - - -def test_sholl_crossings_simple(): - center = SIMPLE.soma.center - radii = [] - assert (list(_nf.sholl_crossings(SIMPLE, center, radii=radii)) == - []) - - radii = [1.0] - assert ([2] == - list(_nf.sholl_crossings(SIMPLE, center, radii=radii))) - - radii = [1.0, 5.1] - assert ([2, 4] == - list(_nf.sholl_crossings(SIMPLE, center, radii=radii))) - - radii = [1., 4., 5.] - assert ([2, 4, 5] == - list(_nf.sholl_crossings(SIMPLE, center, radii=radii))) - - assert ([1, 1, 2] == - list(_nf.sholl_crossings(list(SIMPLE.sections[:2]), center, radii=radii))) - - -def load_swc(string): - with tempfile.NamedTemporaryFile(prefix='test_neuron_func', mode='w', suffix='.swc') as fd: - fd.write(string) - fd.flush() - return load_neuron(fd.name) - - -def test_sholl_analysis_custom(): - # recreate morphs from Fig 2 of - # http://dx.doi.org/10.1016/j.jneumeth.2014.01.016 - radii = np.arange(10, 81, 10) - center = 0, 0, 0 - morph_A = load_swc("""\ - 1 1 0 0 0 1. -1 - 2 3 0 0 0 1. 1 - 3 3 80 0 0 1. 2 - 4 4 0 0 0 1. 1 - 5 4 -80 0 0 1. 4""") - assert (list(_nf.sholl_crossings(morph_A, center, radii=radii)) == - [2, 2, 2, 2, 2, 2, 2, 2]) - - morph_B = load_swc("""\ - 1 1 0 0 0 1. -1 - 2 3 0 0 0 1. 1 - 3 3 35 0 0 1. 2 - 4 3 51 10 0 1. 3 - 5 3 51 5 0 1. 3 - 6 3 51 0 0 1. 3 - 7 3 51 -5 0 1. 3 - 8 3 51 -10 0 1. 3 - 9 4 -35 0 0 1. 2 -10 4 -51 10 0 1. 9 -11 4 -51 5 0 1. 9 -12 4 -51 0 0 1. 9 -13 4 -51 -5 0 1. 9 -14 4 -51 -10 0 1. 9 - """) - assert (list(_nf.sholl_crossings(morph_B, center, radii=radii)) == - [2, 2, 2, 10, 10, 0, 0, 0]) - - morph_C = load_swc("""\ - 1 1 0 0 0 1. -1 - 2 3 0 0 0 1. 1 - 3 3 65 0 0 1. 2 - 4 3 85 10 0 1. 3 - 5 3 85 5 0 1. 3 - 6 3 85 0 0 1. 3 - 7 3 85 -5 0 1. 3 - 8 3 85 -10 0 1. 3 - 9 4 65 0 0 1. 2 -10 4 85 10 0 1. 9 -11 4 85 5 0 1. 9 -12 4 85 0 0 1. 9 -13 4 85 -5 0 1. 9 -14 4 85 -10 0 1. 9 - """) - assert (list(_nf.sholl_crossings(morph_C, center, radii=radii)) == - [2, 2, 2, 2, 2, 2, 10, 10]) - # view.neuron(morph_C)[0].savefig('foo.png') diff --git a/tests/features/test_sectionfunc.py b/tests/features/test_section.py similarity index 59% rename from tests/features/test_sectionfunc.py rename to tests/features/test_section.py index 7eabcec08..7a35c62bc 100644 --- a/tests/features/test_sectionfunc.py +++ b/tests/features/test_section.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Test neurom.sectionfunc functionality.""" +"""Test neurom.sectionfunc.""" import math import warnings @@ -34,47 +34,30 @@ from pathlib import Path import numpy as np -from neurom import load_neuron -from neurom import morphmath as mmth -from neurom.features import neuritefunc as _nf -from neurom.features import sectionfunc as _sf +from neurom import load_morphology, iter_sections +from neurom import morphmath +from neurom.features import section import pytest -from numpy.testing import assert_allclose DATA_PATH = Path(__file__).parent.parent / 'data' H5_PATH = DATA_PATH / 'h5/v1/' SWC_PATH = DATA_PATH / 'swc/' -NRN = load_neuron(H5_PATH / 'Neuron.h5') +NRN = load_morphology(H5_PATH / 'Neuron.h5') SECTION_ID = 0 -def test_total_volume_per_neurite(): - - vol = _nf.total_volume_per_neurite(NRN) - assert len(vol) == 4 - - # calculate the volumes by hand and compare - vol2 = [sum(_sf.section_volume(s) for s in n.iter_sections()) for n in NRN.neurites] - assert vol == vol2 - - # regression test - ref_vol = [271.94122143951864, 281.24754646913954, - 274.98039928781355, 276.73860261723024] - assert_allclose(vol, ref_vol) - - def test_section_area(): - sec = load_neuron(StringIO(u"""((CellBody) (0 0 0 2)) + sec = load_morphology(StringIO(u"""((CellBody) (0 0 0 2)) ((Dendrite) (0 0 0 2) (1 0 0 2))"""), reader='asc').sections[SECTION_ID] - area = _sf.section_area(sec) + area = section.section_area(sec) assert math.pi * 1 * 2 * 1 == area def test_section_tortuosity(): - sec_a = load_neuron(StringIO(u""" + sec_a = load_morphology(StringIO(u""" ((CellBody) (0 0 0 2)) ((Dendrite) (0 0 0 2) @@ -82,7 +65,7 @@ def test_section_tortuosity(): (2 0 0 2) (3 0 0 2))"""), reader='asc').sections[SECTION_ID] - sec_b = load_neuron(StringIO(u""" + sec_b = load_morphology(StringIO(u""" ((CellBody) (0 0 0 2)) ((Dendrite) (0 0 0 2) @@ -90,23 +73,22 @@ def test_section_tortuosity(): (1 2 0 2) (0 2 0 2))"""), reader='asc').sections[SECTION_ID] - assert _sf.section_tortuosity(sec_a) == 1.0 - assert _sf.section_tortuosity(sec_b) == 4.0 / 2.0 + assert section.section_tortuosity(sec_a) == 1.0 + assert section.section_tortuosity(sec_b) == 4.0 / 2.0 - for s in _nf.iter_sections(NRN): - assert (_sf.section_tortuosity(s) == - mmth.section_length(s.points) / mmth.point_dist(s.points[0], - s.points[-1])) + for s in iter_sections(NRN): + assert (section.section_tortuosity(s) == + morphmath.section_length(s.points) / morphmath.point_dist(s.points[0], s.points[-1])) def test_setion_tortuosity_single_point(): - sec = load_neuron(StringIO(u"""((CellBody) (0 0 0 2)) + sec = load_morphology(StringIO(u"""((CellBody) (0 0 0 2)) ((Dendrite) (1 2 3 2))"""), reader='asc').sections[SECTION_ID] - assert _sf.section_tortuosity(sec) == 1.0 + assert section.section_tortuosity(sec) == 1.0 def test_section_tortuosity_looping_section(): - sec = load_neuron(StringIO(u""" + sec = load_morphology(StringIO(u""" ((CellBody) (0 0 0 2)) ((Dendrite) (0 0 0 2) @@ -115,83 +97,72 @@ def test_section_tortuosity_looping_section(): (0 2 0 2) (0 0 0 2))"""), reader='asc').sections[SECTION_ID] with warnings.catch_warnings(record=True): - assert _sf.section_tortuosity(sec) == np.inf + assert section.section_tortuosity(sec) == np.inf def test_section_meander_angles(): - s0 = load_neuron(StringIO(u"""((CellBody) (0 0 0 0)) + s0 = load_morphology(StringIO(u"""((CellBody) (0 0 0 0)) ((Dendrite) (0 0 0 2) (1 0 0 2) (2 0 0 2) (3 0 0 2) (4 0 0 2))"""), reader='asc').sections[SECTION_ID] + assert section.section_meander_angles(s0) == [math.pi, math.pi, math.pi] - assert (_sf.section_meander_angles(s0) == - [math.pi, math.pi, math.pi]) - - s1 = load_neuron(StringIO(u"""((CellBody) (0 0 0 0)) + s1 = load_morphology(StringIO(u"""((CellBody) (0 0 0 0)) ((Dendrite) (0 0 0 2) (1 0 0 2) (1 1 0 2) (2 1 0 2) (2 2 0 2))"""), reader='asc').sections[SECTION_ID] + assert section.section_meander_angles(s1) == [math.pi / 2, math.pi / 2, math.pi / 2] - assert (_sf.section_meander_angles(s1) == - [math.pi / 2, math.pi / 2, math.pi / 2]) - - s2 = load_neuron(StringIO(u"""((CellBody) (0 0 0 0)) + s2 = load_morphology(StringIO(u"""((CellBody) (0 0 0 0)) ((Dendrite) (0 0 0 2) (0 0 1 2) (0 0 2 2) (0 0 0 2))"""), reader='asc').sections[SECTION_ID] - - assert (_sf.section_meander_angles(s2) == - [math.pi, 0.]) + assert section.section_meander_angles(s2) == [math.pi, 0.] def test_section_meander_angles_single_segment(): - s = load_neuron(StringIO(u"""((CellBody) (0 0 0 0)) + s = load_morphology(StringIO(u"""((CellBody) (0 0 0 0)) ((Dendrite) (0 0 0 2) (1 1 1 2))"""), reader='asc').sections[SECTION_ID] - assert len(_sf.section_meander_angles(s)) == 0 + assert len(section.section_meander_angles(s)) == 0 def test_strahler_order(): path = Path(SWC_PATH, 'strahler.swc') - n = load_neuron(path) - strahler_order = _sf.strahler_order(n.neurites[0].root_node) + n = load_morphology(path) + strahler_order = section.strahler_order(n.neurites[0].root_node) assert strahler_order == 4 def test_locate_segment_position(): - s = load_neuron(StringIO(u"""((CellBody) (0 0 0 0)) + s = load_morphology(StringIO(u"""((CellBody) (0 0 0 0)) ((Dendrite) (0 0 0 0) (3 0 4 200) (6 4 4 400))"""), reader='asc').sections[SECTION_ID] - assert ( - _sf.locate_segment_position(s, 0.0) == - (0, 0.0)) - assert ( - _sf.locate_segment_position(s, 0.25) == - (0, 2.5)) - assert ( - _sf.locate_segment_position(s, 0.75) == - (1, 2.5)) - assert ( - _sf.locate_segment_position(s, 1.0) == - (1, 5.0)) + + assert section.locate_segment_position(s, 0.0) == (0, 0.0) + assert section.locate_segment_position(s, 0.25) == (0, 2.5) + assert section.locate_segment_position(s, 0.75) == (1, 2.5) + assert section.locate_segment_position(s, 1.0) == (1, 5.0) + with pytest.raises(ValueError): - _sf.locate_segment_position(s, 1.1) + section.locate_segment_position(s, 1.1) with pytest.raises(ValueError): - _sf.locate_segment_position(s, -0.1) + section.locate_segment_position(s, -0.1) + def test_mean_radius(): - n = load_neuron(StringIO(u""" + n = load_morphology(StringIO(u""" ((CellBody) (0 0 0 1)) @@ -200,6 +171,4 @@ def test_mean_radius(): (3 0 4 200) (6 4 4 400))"""), reader='asc') - assert ( - _sf.section_mean_radius(n.neurites[0]) == - 100.) + assert section.section_mean_radius(n.neurites[0]) == 100. diff --git a/tests/features/utils.py b/tests/features/utils.py deleted file mode 100644 index 05bca8f92..000000000 --- a/tests/features/utils.py +++ /dev/null @@ -1,19 +0,0 @@ -import numpy as np - - -def _close(a, b, debug=False, rtol=1e-05, atol=1e-08): - a, b = list(a), list(b) - if debug: - print('\na.shape: %s\nb.shape: %s\n' % (a.shape, b.shape)) - print('\na: %s\nb:%s\n' % (a, b)) - print('\na - b:%s\n' % (a - b)) - assert len(a) == len(b) - assert np.allclose(a, b, rtol=rtol, atol=atol) - - -def _equal(a, b, debug=False): - if debug: - print('\na.shape: %s\nb.shape: %s\n' % (a.shape, b.shape)) - print('\na: %s\nb:%s\n' % (a, b)) - assert len(a) == len(b) - assert np.alltrue(a == b) diff --git a/tests/geom/test_geom.py b/tests/geom/test_geom.py index 93b676db2..91f6c2bb7 100644 --- a/tests/geom/test_geom.py +++ b/tests/geom/test_geom.py @@ -34,8 +34,8 @@ from numpy.testing import assert_almost_equal SWC_DATA_PATH = Path(__file__).parent.parent / 'data/swc' -NRN = nm.load_neuron(SWC_DATA_PATH / 'Neuron.swc') -SIMPLE = nm.load_neuron(SWC_DATA_PATH / 'simple.swc') +NRN = nm.load_morphology(SWC_DATA_PATH / 'Neuron.swc') +SIMPLE = nm.load_morphology(SWC_DATA_PATH / 'simple.swc') class PointObj: pass @@ -54,7 +54,7 @@ def test_bounding_box(): assert np.alltrue(geom.bounding_box(obj) == [[-100, -2, -3], [42, 55, 33]]) -def test_bounding_box_neuron(): +def test_bounding_box_morphology(): ref = np.array([[-40.32853516, -57.600172, 0.], [64.74726272, 48.51626225, 54.20408797]]) diff --git a/tests/geom/test_transform.py b/tests/geom/test_transform.py index 038f75d1e..88938d664 100644 --- a/tests/geom/test_transform.py +++ b/tests/geom/test_transform.py @@ -31,8 +31,7 @@ import neurom.geom.transform as gtr import numpy as np -from neurom import COLS, load_neuron -from neurom.features import neuritefunc as _nf +from neurom import COLS, load_morphology, iter_sections import pytest from numpy.testing import assert_almost_equal @@ -208,99 +207,96 @@ def test_pivot_rotate_points(): assert np.all(p1 == p2) -def _check_fst_nrn_translate(nrn_a, nrn_b, t): +def _check_morphology_translate(m_a, m_b, t): # soma points assert np.allclose( - (nrn_b.soma.points[:, COLS.XYZ] - nrn_a.soma.points[:, COLS.XYZ]), t) - _check_fst_neurite_translate(nrn_a.neurites, nrn_b.neurites, t) + (m_b.soma.points[:, COLS.XYZ] - m_a.soma.points[:, COLS.XYZ]), t) + _check_neurite_translate(m_a.neurites, m_b.neurites, t) -def _check_fst_neurite_translate(nrts_a, nrts_b, t): +def _check_neurite_translate(nrts_a, nrts_b, t): # neurite sections - for sa, sb in zip(_nf.iter_sections(nrts_a), - _nf.iter_sections(nrts_b)): + for sa, sb in zip(iter_sections(nrts_a), iter_sections(nrts_b)): assert np.allclose((sb.points[:, COLS.XYZ] - sa.points[:, COLS.XYZ]), t) -def test_translate_fst_neuron_swc(): +def test_translate_morphology_swc(): t = np.array([100., 100., 100.]) - nrn = load_neuron(SWC_NRN_PATH) - tnrn = gtr.translate(nrn, t) - _check_fst_nrn_translate(nrn, tnrn, t) + m = load_morphology(SWC_NRN_PATH) + tm = gtr.translate(m, t) + _check_morphology_translate(m, tm, t) -def test_transform_translate_neuron_swc(): +def test_transform_translate_morphology_swc(): t = np.array([100., 100., 100.]) - nrn = load_neuron(SWC_NRN_PATH) - tnrn = nrn.transform(gtr.Translation(t)) - _check_fst_nrn_translate(nrn, tnrn, t) + m = load_morphology(SWC_NRN_PATH) + tm = m.transform(gtr.Translation(t)) + _check_morphology_translate(m, tm, t) -def test_translate_fst_neuron_h5(): - +def test_translate_morphology_h5(): t = np.array([100., 100., 100.]) - nrn = load_neuron(H5_NRN_PATH) - tnrn = gtr.translate(nrn, t) + m = load_morphology(H5_NRN_PATH) + tm = gtr.translate(m, t) - _check_fst_nrn_translate(nrn, tnrn, t) + _check_morphology_translate(m, tm, t) -def test_transform_translate_neuron_h5(): +def test_transform_translate_morphology_h5(): t = np.array([100., 100., 100.]) - nrn = load_neuron(H5_NRN_PATH) - tnrn = nrn.transform(gtr.Translation(t)) - _check_fst_nrn_translate(nrn, tnrn, t) + m = load_morphology(H5_NRN_PATH) + tm = m.transform(gtr.Translation(t)) + _check_morphology_translate(m, tm, t) def _apply_rot(points, rot_mat): return np.dot(rot_mat, np.array(points).T).T -def _check_fst_nrn_rotate(nrn_a, nrn_b, rot_mat): +def _check_morphology_rotate(m_a, m_b, rot_mat): # soma points - assert np.allclose(_apply_rot(nrn_a.soma.points[:, COLS.XYZ], rot_mat), - nrn_b.soma.points[:, COLS.XYZ]) + assert np.allclose(_apply_rot(m_a.soma.points[:, COLS.XYZ], rot_mat), + m_b.soma.points[:, COLS.XYZ]) # neurite sections - _check_fst_neurite_rotate(nrn_a.neurites, nrn_b.neurites, rot_mat) + _check_neurite_rotate(m_a.neurites, m_b.neurites, rot_mat) -def _check_fst_neurite_rotate(nrt_a, nrt_b, rot_mat): - for sa, sb in zip(_nf.iter_sections(nrt_a), - _nf.iter_sections(nrt_b)): +def _check_neurite_rotate(nrt_a, nrt_b, rot_mat): + for sa, sb in zip(iter_sections(nrt_a), iter_sections(nrt_b)): assert np.allclose(sb.points[:, COLS.XYZ], _apply_rot(sa.points[:, COLS.XYZ], rot_mat)) -def test_rotate_neuron_swc(): - nrn_a = load_neuron(SWC_NRN_PATH) - nrn_b = gtr.rotate(nrn_a, [0, 0, 1], math.pi/2.0) +def test_rotate_morphology_swc(): + m_a = load_morphology(SWC_NRN_PATH) + m_b = gtr.rotate(m_a, [0, 0, 1], math.pi/2.0) rot = gtr._rodrigues_to_dcm([0, 0, 1], math.pi/2.0) - _check_fst_nrn_rotate(nrn_a, nrn_b, rot) + _check_morphology_rotate(m_a, m_b, rot) -def test_transform_rotate_neuron_swc(): +def test_transform_rotate_morphology_swc(): rot = gtr.Rotation(ROT_90) - nrn_a = load_neuron(SWC_NRN_PATH) - nrn_b = nrn_a.transform(rot) - _check_fst_nrn_rotate(nrn_a, nrn_b, ROT_90) + m_a = load_morphology(SWC_NRN_PATH) + m_b = m_a.transform(rot) + _check_morphology_rotate(m_a, m_b, ROT_90) -def test_rotate_neuron_h5(): - nrn_a = load_neuron(H5_NRN_PATH) - nrn_b = gtr.rotate(nrn_a, [0, 0, 1], math.pi/2.0) +def test_rotate_morphology_h5(): + m_a = load_morphology(H5_NRN_PATH) + m_b = gtr.rotate(m_a, [0, 0, 1], math.pi/2.0) rot = gtr._rodrigues_to_dcm([0, 0, 1], math.pi/2.0) - _check_fst_nrn_rotate(nrn_a, nrn_b, rot) + _check_morphology_rotate(m_a, m_b, rot) -def test_transform_rotate_neuron_h5(): +def test_transform_rotate_morphology_h5(): rot = gtr.Rotation(ROT_90) - nrn_a = load_neuron(H5_NRN_PATH) - nrn_b = nrn_a.transform(rot) - _check_fst_nrn_rotate(nrn_a, nrn_b, ROT_90) + m_a = load_morphology(H5_NRN_PATH) + m_b = m_a.transform(rot) + _check_morphology_rotate(m_a, m_b, ROT_90) def test_rodrigues_to_dcm(): diff --git a/tests/io/test_io_utils.py b/tests/io/test_io_utils.py index 95814ad33..0efc94377 100644 --- a/tests/io/test_io_utils.py +++ b/tests/io/test_io_utils.py @@ -34,8 +34,8 @@ import numpy as np from morphio import MissingParentError, RawDataError, SomaError, UnknownFileType, MorphioError, \ set_raise_warnings -from neurom import COLS, get, load_neuron -from neurom.core.neuron import Neuron +from neurom import COLS, get, load_morphology +from neurom.core.morphology import Morphology from neurom.exceptions import NeuroMError from neurom.io import utils import pytest @@ -56,15 +56,15 @@ 'Neuron_no_missing_ids_no_zero_segs.swc']] FILENAMES = [VALID_DATA_PATH / f for f in ['Neuron.swc', 'Neuron_h5v1.h5']] -NRN = utils.load_neuron(VALID_DATA_PATH / 'Neuron.swc') +NRN = utils.load_morphology(VALID_DATA_PATH / 'Neuron.swc') NO_SOMA_FILE = SWC_PATH / 'Single_apical_no_soma.swc' DISCONNECTED_POINTS_FILE = SWC_PATH / 'Neuron_disconnected_components.swc' MISSING_PARENTS_FILE = SWC_PATH / 'Neuron_missing_parents.swc' -def _check_neurites_have_no_parent(nrn): +def _check_neurites_have_no_parent(m): - for n in nrn.neurites: + for n in m.neurites: assert n.root_node.parent is None @@ -75,61 +75,61 @@ def test_get_morph_files(): assert ref == files -def test_load_neurons(): +def test_load_morphologies(): # List of strings - nrns = utils.load_neurons(list(map(str, FILES))) - for i, nrn in enumerate(nrns): - assert nrn.name == FILES[i].name + pop = utils.load_morphologies(list(map(str, FILES))) + for i, m in enumerate(pop): + assert m.name == FILES[i].name with pytest.raises(NeuroMError): - list(utils.load_neurons(MISSING_PARENTS_FILE,)) + list(utils.load_morphologies(MISSING_PARENTS_FILE, )) # Single string - nrns = utils.load_neurons(str(FILES[0])) - assert nrns[0].name == FILES[0].name + pop = utils.load_morphologies(str(FILES[0])) + assert pop[0].name == FILES[0].name # Single Path - nrns = utils.load_neurons(FILES[0]) - assert nrns[0].name == FILES[0].name + pop = utils.load_morphologies(FILES[0]) + assert pop[0].name == FILES[0].name # list of strings - nrns = utils.load_neurons(list(map(str, FILES))) - for i, nrn in enumerate(nrns): - assert nrn.name == FILES[i].name + pop = utils.load_morphologies(list(map(str, FILES))) + for i, m in enumerate(pop): + assert m.name == FILES[i].name # sequence of Path objects - nrns = utils.load_neurons(FILES) - for nrn, file in zip(nrns, FILES): - assert nrn.name == file.name + pop = utils.load_morphologies(FILES) + for m, file in zip(pop, FILES): + assert m.name == file.name # string path to a directory - nrns = utils.load_neurons(str(SWC_PATH), ignored_exceptions=(MissingParentError, MorphioError)) + pop = utils.load_morphologies(str(SWC_PATH), ignored_exceptions=(MissingParentError, MorphioError)) # is subset so that if new morpho are added to SWC_PATH, the test does not break - assert {f.name for f in FILES}.issubset({nrn.name for nrn in nrns}) + assert {f.name for f in FILES}.issubset({m.name for m in pop}) # Path path to a directory - nrns = utils.load_neurons(SWC_PATH, ignored_exceptions=(MissingParentError, MorphioError)) + pop = utils.load_morphologies(SWC_PATH, ignored_exceptions=(MissingParentError, MorphioError)) # is subset so that if new morpho are added to SWC_PATH, the test does not break - assert {f.name for f in FILES}.issubset({nrn.name for nrn in nrns}) + assert {f.name for f in FILES}.issubset({m.name for m in pop}) def test_ignore_exceptions(): with pytest.raises(NeuroMError): - list(utils.load_neurons(MISSING_PARENTS_FILE,)) + list(utils.load_morphologies(MISSING_PARENTS_FILE, )) count = 0 - pop = utils.load_neurons((MISSING_PARENTS_FILE,), ignored_exceptions=(RawDataError,)) + pop = utils.load_morphologies((MISSING_PARENTS_FILE,), ignored_exceptions=(RawDataError,)) for _ in pop: count += 1 assert count == 0 -def test_load_neuron(): - nrn = utils.load_neuron(FILENAMES[0]) - assert isinstance(NRN, Neuron) +def test_load_morphology(): + m = utils.load_morphology(FILENAMES[0]) + assert isinstance(NRN, Morphology) assert NRN.name == 'Neuron.swc' - _check_neurites_have_no_parent(nrn) + _check_neurites_have_no_parent(m) - neuron_str = u""" 1 1 0 0 0 1. -1 + morphology_str = u""" 1 1 0 0 0 1. -1 2 3 0 0 0 1. 1 3 3 0 5 0 1. 2 4 3 -5 5 0 0. 3 @@ -139,27 +139,27 @@ def test_load_neuron(): 8 2 6 -4 0 0. 7 9 2 -5 -4 0 0. 7 """ - utils.load_neuron(StringIO(neuron_str), reader='swc') + utils.load_morphology(StringIO(morphology_str), reader='swc') -def test_neuron_name(): +def test_morphology_name(): for fn, nn in zip(FILENAMES, NRN_NAMES): - nrn = utils.load_neuron(fn) - assert nrn.name == nn + m = utils.load_morphology(fn) + assert m.name == nn def test_load_bifurcating_soma_points_raises_SomaError(): with pytest.raises(SomaError): - utils.load_neuron(Path(SWC_PATH, 'soma', 'bifurcating_soma.swc')) + utils.load_morphology(Path(SWC_PATH, 'soma', 'bifurcating_soma.swc')) def test_load_neuromorpho_3pt_soma(): with warnings.catch_warnings(record=True): - nrn = utils.load_neuron(Path(SWC_PATH, 'soma', 'three_pt_soma.swc')) - assert len(nrn.neurites) == 4 - assert len(nrn.soma.points) == 3 - assert nrn.soma.radius == 2 - _check_neurites_have_no_parent(nrn) + m = utils.load_morphology(Path(SWC_PATH, 'soma', 'three_pt_soma.swc')) + assert len(m.neurites) == 4 + assert len(m.soma.points) == 3 + assert m.soma.radius == 2 + _check_neurites_have_no_parent(m) def test_neurites_have_no_parent(): @@ -167,12 +167,12 @@ def test_neurites_have_no_parent(): _check_neurites_have_no_parent(NRN) -def test_neuron_sections(): +def test_morphology_sections(): # check no duplicates assert len(set(NRN.sections)) == len(list(NRN.sections)) -def test_neuron_sections_are_connected(): +def test_morphology_sections_are_connected(): # check traversal by counting number of sections un trees for nrt in NRN.neurites: root_node = nrt.root_node @@ -180,97 +180,88 @@ def test_neuron_sections_are_connected(): sum(1 for _ in NRN.sections[root_node.id].ipreorder())) -def test_load_neuron_soma_only(): +def test_load_morphology_soma_only(): - nrn = utils.load_neuron(Path(DATA_PATH, 'swc', 'Soma_origin.swc')) - assert len(nrn.neurites) == 0 - assert nrn.name == 'Soma_origin.swc' + m = utils.load_morphology(Path(DATA_PATH, 'swc', 'Soma_origin.swc')) + assert len(m.neurites) == 0 + assert m.name == 'Soma_origin.swc' -def test_load_neuron_disconnected_points_raises(): +def test_load_morphology_disconnected_points_raises(): try: set_raise_warnings(True) with pytest.raises(MorphioError, match='Warning: found a disconnected neurite'): - load_neuron(DISCONNECTED_POINTS_FILE) + load_morphology(DISCONNECTED_POINTS_FILE) finally: set_raise_warnings(False) -def test_load_neuron_missing_parents_raises(): +def test_load_morphology_missing_parents_raises(): with pytest.raises(MissingParentError): - utils.load_neuron(MISSING_PARENTS_FILE) + utils.load_morphology(MISSING_PARENTS_FILE) -def test_load_neurons_directory(): - pop = utils.load_neurons(VALID_DATA_PATH) +def test_load_morphologies_directory(): + pop = utils.load_morphologies(VALID_DATA_PATH) assert len(pop) == 4 assert pop.name == 'valid_set' - for nrn in pop: - assert isinstance(nrn, Neuron) + for m in pop: + assert isinstance(m, Morphology) -def test_load_neurons_directory_name(): - pop = utils.load_neurons(VALID_DATA_PATH, name='test123') +def test_load_morphologies_directory_name(): + pop = utils.load_morphologies(VALID_DATA_PATH, name='test123') assert len(pop) == 4 assert pop.name == 'test123' - for nrn in pop: - assert isinstance(nrn, Neuron) + for m in pop: + assert isinstance(m, Morphology) -def test_load_neurons_filenames(): - pop = utils.load_neurons(FILENAMES, name='test123') +def test_load_morphologies_filenames(): + pop = utils.load_morphologies(FILENAMES, name='test123') assert len(pop) == 2 assert pop.name == 'test123' - for nrn, name in zip(pop.neurons, NRN_NAMES): - assert isinstance(nrn, Neuron) - assert nrn.name == name + for m, name in zip(pop.morphologies, NRN_NAMES): + assert isinstance(m, Morphology) + assert m.name == name SWC_ORD_PATH = Path(DATA_PATH, 'swc', 'ordering') -SWC_ORD_REF = utils.load_neuron(Path(SWC_ORD_PATH, 'sample.swc')) +SWC_ORD_REF = utils.load_morphology(Path(SWC_ORD_PATH, 'sample.swc')) def assert_items_equal(a, b): assert sorted(a) == sorted(b) -def test_load_neuron_mixed_tree_swc(): - nrn_mix = utils.load_neuron(Path(SWC_ORD_PATH, 'sample_mixed_tree_sections.swc')) - assert_items_equal(get('number_of_sections_per_neurite', nrn_mix), [5, 3]) +def test_load_morphology_mixed_tree_swc(): + m_mix = utils.load_morphology(Path(SWC_ORD_PATH, 'sample_mixed_tree_sections.swc')) - assert_items_equal(get('number_of_sections_per_neurite', nrn_mix), + assert_items_equal(get('number_of_sections_per_neurite', m_mix), [5, 3]) + assert_items_equal(get('number_of_sections_per_neurite', m_mix), get('number_of_sections_per_neurite', SWC_ORD_REF)) + assert get('number_of_segments', m_mix) == get('number_of_segments', SWC_ORD_REF) + assert get('total_length', m_mix) == get('total_length', SWC_ORD_REF) - assert_items_equal(get('number_of_segments', nrn_mix), - get('number_of_segments', SWC_ORD_REF)) - assert_items_equal(get('total_length', nrn_mix), - get('total_length', SWC_ORD_REF)) +def test_load_morphology_section_order_break_swc(): + m_mix = utils.load_morphology(Path(SWC_ORD_PATH, 'sample_disordered.swc')) - -def test_load_neuron_section_order_break_swc(): - nrn_mix = utils.load_neuron(Path(SWC_ORD_PATH, 'sample_disordered.swc')) - - assert_items_equal(get('number_of_sections_per_neurite', nrn_mix), [5, 3]) - - assert_items_equal(get('number_of_sections_per_neurite', nrn_mix), + assert_items_equal(get('number_of_sections_per_neurite', m_mix), [5, 3]) + assert_items_equal(get('number_of_sections_per_neurite', m_mix), get('number_of_sections_per_neurite', SWC_ORD_REF)) - - assert_items_equal(get('number_of_segments', nrn_mix), - get('number_of_segments', SWC_ORD_REF)) - - assert_items_equal(get('total_length', nrn_mix), - get('total_length', SWC_ORD_REF)) + assert get('number_of_segments', m_mix) == get('number_of_segments', SWC_ORD_REF) + assert get('total_length', m_mix) == get('total_length', SWC_ORD_REF) H5_PATH = Path(DATA_PATH, 'h5', 'v1', 'ordering') -H5_ORD_REF = utils.load_neuron(Path(H5_PATH, 'sample.h5')) +H5_ORD_REF = utils.load_morphology(Path(H5_PATH, 'sample.h5')) -def test_load_neuron_mixed_tree_h5(): - nrn_mix = utils.load_neuron(Path(H5_PATH, 'sample_mixed_tree_sections.h5')) - assert_items_equal(get('number_of_sections_per_neurite', nrn_mix), [5, 3]) - assert_items_equal(get('number_of_sections_per_neurite', nrn_mix), +def test_load_morphology_mixed_tree_h5(): + m_mix = utils.load_morphology(Path(H5_PATH, 'sample_mixed_tree_sections.h5')) + assert_items_equal(get('number_of_sections_per_neurite', m_mix), [5, 3]) + assert_items_equal(get('number_of_sections_per_neurite', m_mix), get('number_of_sections_per_neurite', H5_ORD_REF)) @@ -279,37 +270,37 @@ def test_load_h5_trunk_points_regression(): # implementing PR #479, related to H5 unpacking # of files with non-standard soma structure. # See #480. - nrn = utils.load_neuron(Path(DATA_PATH, 'h5', 'v1', 'Neuron.h5')) - assert np.allclose(nrn.neurites[0].root_node.points[1, COLS.XYZR], + m = utils.load_morphology(Path(DATA_PATH, 'h5', 'v1', 'Neuron.h5')) + assert np.allclose(m.neurites[0].root_node.points[1, COLS.XYZR], [0., 0., 0.1, 0.31646374]) - assert np.allclose(nrn.neurites[1].root_node.points[1, COLS.XYZR], + assert np.allclose(m.neurites[1].root_node.points[1, COLS.XYZR], [0., 0., 0.1, 1.84130445e-01]) - assert np.allclose(nrn.neurites[2].root_node.points[1, COLS.XYZR], + assert np.allclose(m.neurites[2].root_node.points[1, COLS.XYZR], [0., 0., 0.1, 5.62225521e-01]) - assert np.allclose(nrn.neurites[3].root_node.points[1, COLS.XYZR], + assert np.allclose(m.neurites[3].root_node.points[1, COLS.XYZR], [0., 0., 0.1, 7.28555262e-01]) def test_load_unknown_type(): with pytest.raises(UnknownFileType): - load_neuron(DATA_PATH / 'unsupported_extension.fake') + load_morphology(DATA_PATH / 'unsupported_extension.fake') def test_NeuronLoader(): dirpath = Path(DATA_PATH, 'h5', 'v1') - loader = utils.NeuronLoader(dirpath, file_ext='.h5', cache_size=5) - nrn = loader.get('Neuron') - assert isinstance(nrn, Neuron) + loader = utils.MorphLoader(dirpath, file_ext='.h5', cache_size=5) + m = loader.get('Neuron') + assert isinstance(m, Morphology) # check caching - assert nrn == loader.get('Neuron') - assert nrn != loader.get('Neuron_2_branch') + assert m == loader.get('Neuron') + assert m != loader.get('Neuron_2_branch') def test_NeuronLoader_mixed_file_extensions(): - loader = utils.NeuronLoader(VALID_DATA_PATH) + loader = utils.MorphLoader(VALID_DATA_PATH) loader.get('Neuron') loader.get('Neuron_h5v1') with pytest.raises(NeuroMError): @@ -320,12 +311,12 @@ def test_get_files_by_path(): single_neurom = utils.get_files_by_path(NO_SOMA_FILE) assert len(single_neurom) == 1 - neuron_dir = utils.get_files_by_path(VALID_DATA_PATH) - assert len(neuron_dir) == 4 + morphologies_dir = utils.get_files_by_path(VALID_DATA_PATH) + assert len(morphologies_dir) == 4 with pytest.raises(IOError): utils.get_files_by_path(Path('this/is/a/fake/path')) def test_h5v2_raises(): with pytest.raises(RawDataError): - utils.load_neuron(DATA_PATH / 'h5/v2/Neuron.h5') + utils.load_morphology(DATA_PATH / 'h5/v2/Neuron.h5') diff --git a/tests/io/test_neurolucida.py b/tests/io/test_neurolucida.py index b951ff369..6853f20ee 100644 --- a/tests/io/test_neurolucida.py +++ b/tests/io/test_neurolucida.py @@ -4,7 +4,7 @@ import numpy as np from morphio import RawDataError, SomaError import neurom as nm -from neurom import load_neuron +from neurom import load_morphology import pytest from numpy.testing import assert_array_equal @@ -23,7 +23,7 @@ def test_soma(): (-1 -1 0 2 S3) ) """ - n = nm.load_neuron(string_section, reader='asc') + n = nm.load_morphology(string_section, reader='asc') assert_array_equal(n.soma.points, [[1, 1, 0, 0.5], @@ -44,7 +44,7 @@ def test_unknown_token(): (-1 -1 0 2 S3) ) """ - n = nm.load_neuron(string_section, reader='asc') + n = nm.load_morphology(string_section, reader='asc') assert obj.match("Unexpected token: Z") assert obj.match(":6:error") @@ -56,7 +56,7 @@ def test_unfinished_point(): (Color Red) (CellBody) (1 1""" - n = nm.load_neuron(string_section, reader='asc') + n = nm.load_morphology(string_section, reader='asc') assert obj.match('Error converting: "" to float') assert obj.match(':4:error') @@ -81,7 +81,7 @@ def test_multiple_soma(): (-1 -1 0 2 S3) ) """ - load_neuron(string_section, reader='asc') + load_morphology(string_section, reader='asc') assert obj.match("A soma is already defined") assert obj.match(':16:error') @@ -100,7 +100,7 @@ def test_single_neurite_no_soma(): Generated ) ; End of tree""" - n = nm.load_neuron(string_section, reader='asc') + n = nm.load_morphology(string_section, reader='asc') assert_array_equal(n.soma.points, np.empty((0, 4))) assert len(n.neurites) == 1 @@ -111,7 +111,7 @@ def test_single_neurite_no_soma(): def test_skip_header(): """Test that the header does not cause any issue""" - str_neuron = """(FilledCircle + str_morph = """(FilledCircle (Color RGB (64, 0, 128)) (Name "Marker 11") (Set "axons") @@ -125,7 +125,7 @@ def test_skip_header(): ( 1.2 3.7 2.0 13) )""" - n = nm.load_neuron(str_neuron, reader='asc') + n = nm.load_morphology(str_morph, reader='asc') assert len(n.neurites) == 1 assert_array_equal(n.neurites[0].points, np.array([[1.2, 2.7, 1.0, 6.5], @@ -172,7 +172,7 @@ def test_read_with_duplicates(): # what I think the # https://developer.humanbrainproject.eu/docs/projects/morphology-documentation/0.0.2/h5v1.html # would look like - n = load_neuron(StringIO(with_duplicate), reader='asc') + n = load_morphology(StringIO(with_duplicate), reader='asc') assert len(n.neurites) == 1 @@ -205,8 +205,8 @@ def test_read_with_duplicates(): def test_read_without_duplicates(): - n_with_duplicate = load_neuron(with_duplicate, reader='asc') - n_without_duplicate = load_neuron(without_duplicate, reader='asc') + n_with_duplicate = load_morphology(with_duplicate, reader='asc') + n_without_duplicate = load_morphology(without_duplicate, reader='asc') assert_array_equal(n_with_duplicate.neurites[0].root_node.children[0].points, n_without_duplicate.neurites[0].root_node.children[0].points) @@ -217,7 +217,7 @@ def test_read_without_duplicates(): def test_unfinished_file(): with pytest.raises(RawDataError) as obj: - load_neuron(""" + load_morphology(""" ((Dendrite) (3 -4 0 2) (3 -6 0 2) @@ -234,7 +234,7 @@ def test_unfinished_file(): def test_empty_sibling(): - n = load_neuron(""" + n = load_morphology(""" ((Dendrite) (3 -4 0 2) (3 -6 0 2) @@ -260,7 +260,7 @@ def test_empty_sibling(): def test_single_children(): - n = load_neuron(StringIO( + n = load_morphology(StringIO( """ ((Dendrite) (3 -4 0 2) @@ -291,7 +291,7 @@ def test_single_children(): def test_markers(): """Test that markers do not prevent file from being read correctly""" - n = load_neuron(""" + n = load_morphology(""" ( (Color White) ; [10,1] (Dendrite) ( -290.87 -113.09 -16.32 2.06) ; Root diff --git a/tests/io/test_swc_reader.py b/tests/io/test_swc_reader.py index 5637f7a30..9eed5ae1b 100644 --- a/tests/io/test_swc_reader.py +++ b/tests/io/test_swc_reader.py @@ -31,7 +31,7 @@ import numpy as np from morphio import RawDataError, MorphioError -from neurom import load_neuron, NeuriteType +from neurom import load_morphology, NeuriteType import pytest from numpy.testing import assert_array_equal @@ -43,16 +43,16 @@ def test_repeated_id(): with pytest.raises(RawDataError): - load_neuron(SWC_PATH / 'repeated_id.swc') + load_morphology(SWC_PATH / 'repeated_id.swc') def test_neurite_followed_by_soma(): with pytest.raises(MorphioError, match='Found a soma point with a neurite as parent'): - load_neuron(SWC_PATH / 'soma_with_neurite_parent.swc') + load_morphology(SWC_PATH / 'soma_with_neurite_parent.swc') def test_read_single_neurite(): - n = load_neuron(SWC_PATH / 'point_soma_single_neurite.swc') + n = load_morphology(SWC_PATH / 'point_soma_single_neurite.swc') assert len(n.neurites) == 1 assert n.neurites[0].root_node.id == 0 assert_array_equal(n.soma.points, @@ -67,7 +67,7 @@ def test_read_single_neurite(): def test_read_split_soma(): - n = load_neuron(SWC_PATH / 'split_soma_two_neurites.swc') + n = load_morphology(SWC_PATH / 'split_soma_two_neurites.swc') assert_array_equal(n.soma.points, [[1, 0, 1, 4.0], @@ -92,7 +92,7 @@ def test_read_split_soma(): def test_weird_indent(): - n = load_neuron(""" + n = load_morphology(""" # this is the same as simple.swc @@ -114,14 +114,14 @@ def test_weird_indent(): 9 2 -5 -4 0 0. 7 """, reader='swc') - simple = load_neuron(SWC_PATH / 'simple.swc') + simple = load_morphology(SWC_PATH / 'simple.swc') assert_array_equal(simple.points, n.points) def test_cyclic(): with pytest.raises(RawDataError): - load_neuron(""" + load_morphology(""" 1 1 0 0 0 1. -1 2 3 0 0 0 1. 1 3 3 0 5 0 1. 2 @@ -134,7 +134,7 @@ def test_cyclic(): def test_simple_reversed(): - n = load_neuron(SWC_PATH / 'simple_reversed.swc') + n = load_morphology(SWC_PATH / 'simple_reversed.swc') assert_array_equal(n.soma.points, [[0, 0, 0, 1]]) assert len(n.neurites) == 2 @@ -152,10 +152,10 @@ def test_simple_reversed(): def test_custom_type(): - neuron = load_neuron(Path(SWC_PATH, 'custom_type.swc')) - assert neuron.neurites[1].type == NeuriteType.custom5 + m = load_morphology(Path(SWC_PATH, 'custom_type.swc')) + assert m.neurites[1].type == NeuriteType.custom5 def test_undefined_type(): with pytest.raises(RawDataError, match='Unsupported section type: 0'): - load_neuron(SWC_PATH / 'undefined_type.swc') + load_morphology(SWC_PATH / 'undefined_type.swc') diff --git a/tests/test_utils.py b/tests/test_utils.py index 9bc2d366f..f01a17211 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -28,7 +28,6 @@ """Test neurom.utils.""" import json -import random import warnings import numpy as np @@ -36,26 +35,6 @@ import pytest -def test_memoize_caches(): - class A: - @nu.memoize - def dummy(self, x, y=42): - return random.random() - - a = A() - ref1 = a.dummy(42) - ref2 = a.dummy(42, 43) - ref3 = a.dummy(42, y=43) - - for _ in range(10): - assert a.dummy(42) == ref1 - assert A().dummy(42) != ref1 - assert a.dummy(42, 43) == ref2 - assert A().dummy(42, 43) != ref2 - assert a.dummy(42, y=43) == ref3 - assert A().dummy(42, y=43) != ref3 - - def test_deprecated(): @nu.deprecated(msg='Hello') def dummy(): @@ -69,7 +48,7 @@ def dummy(): def test_deprecated_module(): with warnings.catch_warnings(record=True) as s: - nu.deprecated_module('foo', msg='msg') + nu.deprecated_module('msg') assert len(s) > 0 diff --git a/tests/test_viewer.py b/tests/test_viewer.py index 90c31e29a..2f843ddbd 100644 --- a/tests/test_viewer.py +++ b/tests/test_viewer.py @@ -27,123 +27,88 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import os -import sys import tempfile from pathlib import Path import matplotlib -import mock if 'DISPLAY' not in os.environ: # noqa matplotlib.use('Agg') # noqa -import neurom -from neurom import NeuriteType, load_neuron, viewer -from neurom.view import common, plotly +from neurom import NeuriteType, load_morphology, viewer +from neurom.view import matplotlib_utils import pytest from numpy.testing import assert_allclose DATA_PATH = Path(__file__).parent / 'data/swc' MORPH_FILENAME = DATA_PATH / 'Neuron.swc' -nrn = load_neuron(MORPH_FILENAME) +m = load_morphology(MORPH_FILENAME) -def _reload_module(module): - """Force module reload.""" - import importlib - importlib.reload(module) - - -def test_plotly_extra_not_installed(): - with mock.patch.dict(sys.modules, {'plotly': None}): - try: - _reload_module(neurom.view.plotly) - assert False, "ImportError not triggered" - except ImportError as e: - assert (str(e) == - 'neurom[plotly] is not installed. ' - 'Please install it by doing: pip install neurom[plotly]') - - -def test_plotly_draw_neuron3d(): - plotly.draw(nrn, plane='3d', auto_open=False) - plotly.draw(nrn.neurites[0], plane='3d', auto_open=False) - - fig = plotly.draw(load_neuron(DATA_PATH / 'simple-different-soma.swc'), - auto_open=False) - x, y, z = [fig['data'][2][key] for key in str('xyz')] - assert_allclose(x[0, 0], 2) - assert_allclose(x[33, 33], -1.8971143170299758) - assert_allclose(y[0, 0], 3) - assert_allclose(y[33, 33], 9.75) - assert_allclose(z[0, 0], 13) - assert_allclose(z[33, 33], 8.5) - -def test_plotly_draw_neuron2d(): - plotly.draw(nrn, plane='xy', auto_open=False) - plotly.draw(nrn.neurites[0], plane='xy', auto_open=False) - - -def test_draw_neuron(): - viewer.draw(nrn) - common.plt.close('all') +def test_draw_morphology(): + viewer.draw(m) + matplotlib_utils.plt.close('all') def test_draw_filter_neurite(): for mode in ['2d', '3d']: - viewer.draw(nrn, mode=mode, neurite_type=NeuriteType.basal_dendrite) - assert_allclose(common.plt.gca().get_ylim(), + viewer.draw(m, mode=mode, neurite_type=NeuriteType.basal_dendrite) + assert_allclose(matplotlib_utils.plt.gca().get_ylim(), [-30., 78], atol=5) - common.plt.close('all') + matplotlib_utils.plt.close('all') -def test_draw_neuron3d(): - viewer.draw(nrn, mode='3d') - common.plt.close('all') +def test_draw_morphology3d(): + viewer.draw(m, mode='3d') + matplotlib_utils.plt.close('all') with pytest.raises(NotImplementedError): - viewer.draw(nrn, mode='3d', realistic_diameters=True) + viewer.draw(m, mode='3d', realistic_diameters=True) + + # for coverage + viewer.draw(m, mode='3d', realistic_diameters=False) + matplotlib_utils.plt.close('all') def test_draw_tree(): - viewer.draw(nrn.neurites[0]) - common.plt.close('all') + viewer.draw(m.neurites[0]) + matplotlib_utils.plt.close('all') def test_draw_tree3d(): - viewer.draw(nrn.neurites[0], mode='3d') - common.plt.close('all') + viewer.draw(m.neurites[0], mode='3d') + matplotlib_utils.plt.close('all') def test_draw_soma(): - viewer.draw(nrn.soma) - common.plt.close('all') + viewer.draw(m.soma) + matplotlib_utils.plt.close('all') def test_draw_soma3d(): - viewer.draw(nrn.soma, mode='3d') - common.plt.close('all') + viewer.draw(m.soma, mode='3d') + matplotlib_utils.plt.close('all') def test_draw_dendrogram(): - viewer.draw(nrn, mode='dendrogram') - common.plt.close('all') + viewer.draw(m, mode='dendrogram') + matplotlib_utils.plt.close('all') - viewer.draw(nrn.neurites[0], mode='dendrogram') - common.plt.close('all') + viewer.draw(m.neurites[0], mode='dendrogram') + matplotlib_utils.plt.close('all') def test_draw_dendrogram_empty_segment(): - neuron = load_neuron(DATA_PATH / 'empty_segments.swc') - viewer.draw(neuron, mode='dendrogram') - common.plt.close('all') + m = load_morphology(DATA_PATH / 'empty_segments.swc') + viewer.draw(m, mode='dendrogram') + matplotlib_utils.plt.close('all') def test_invalid_draw_mode_raises(): with pytest.raises(viewer.InvalidDrawModeError): - viewer.draw(nrn, mode='4d') + viewer.draw(m, mode='4d') def test_invalid_object_raises(): @@ -155,12 +120,12 @@ class Dummy: def test_invalid_combo_raises(): with pytest.raises(viewer.NotDrawableError): - viewer.draw(nrn.soma, mode='dendrogram') + viewer.draw(m.soma, mode='dendrogram') def test_writing_output(): with tempfile.TemporaryDirectory() as folder: output_dir = Path(folder, 'subdir') - viewer.draw(nrn, mode='2d', output_path=output_dir) + viewer.draw(m, mode='2d', output_path=output_dir) assert (output_dir / 'Figure.png').is_file() - common.plt.close('all') + matplotlib_utils.plt.close('all') diff --git a/tests/view/conftest.py b/tests/view/conftest.py index 04f017bbb..414ffa353 100644 --- a/tests/view/conftest.py +++ b/tests/view/conftest.py @@ -4,10 +4,10 @@ matplotlib.use('Agg') # noqa -from neurom.view import common -common._get_plt() +from neurom.view import matplotlib_utils +matplotlib_utils._get_plt() -from neurom.view.common import plt +from neurom.view.matplotlib_utils import plt import pytest diff --git a/tests/view/test_dendrogram.py b/tests/view/test_dendrogram.py index 723aeb31a..df7611495 100644 --- a/tests/view/test_dendrogram.py +++ b/tests/view/test_dendrogram.py @@ -3,7 +3,7 @@ import numpy as np import neurom.view.dendrogram as dm -from neurom import load_neuron, get +from neurom import load_morphology, get from neurom.core.types import NeuriteType from numpy.testing import assert_array_almost_equal @@ -12,9 +12,9 @@ NEURON_PATH = DATA_PATH / 'h5/v1/Neuron.h5' -def test_create_dendrogram_neuron(): - neuron = load_neuron(NEURON_PATH) - dendrogram = dm.Dendrogram(neuron) +def test_create_dendrogram_morphology(): + m = load_morphology(NEURON_PATH) + dendrogram = dm.Dendrogram(m) assert NeuriteType.soma == dendrogram.neurite_type soma_len = 1.0 assert soma_len == dendrogram.height @@ -22,7 +22,7 @@ def test_create_dendrogram_neuron(): assert_array_almost_equal( [[-.5, 0], [-.5, soma_len], [.5, soma_len], [.5, 0]], dendrogram.coords) - assert len(neuron.neurites) == len(dendrogram.children) + assert len(m.neurites) == len(dendrogram.children) def test_dendrogram_get_coords(): @@ -41,8 +41,8 @@ def assert_trees(neurom_section, dendrogram): section = neurom_section.children[i] assert section.type == d.neurite_type - neuron = load_neuron(NEURON_PATH) - neurite = neuron.neurites[0] + m = load_morphology(NEURON_PATH) + neurite = m.neurites[0] dendrogram = dm.Dendrogram(neurite) assert neurite.type == dendrogram.neurite_type assert_trees(neurite.root_node, dendrogram) @@ -82,17 +82,15 @@ def assert_layout(dendrogram): .5 * (next_child.width + child.width)) assert_layout(child) - neuron = load_neuron(NEURON_PATH) - dendrogram = dm.Dendrogram(neuron) + m = load_morphology(NEURON_PATH) + dendrogram = dm.Dendrogram(m) positions = dm.layout_dendrogram(dendrogram, np.array([0, 0])) assert_layout(dendrogram) -def test_neuron_not_corrupted(): - # Regression for #492: dendrogram was corrupting - # neuron used to construct it. - # This caused the section path distance calculation - # to raise a KeyError exception. - neuron = load_neuron(NEURON_PATH) - dm.Dendrogram(neuron) - assert get('section_path_distances', neuron).size > 0 +def test_morphology_not_corrupted(): + # Regression for #492: dendrogram was corrupting morphology used to construct it. + # This caused the section path distance calculation to raise a KeyError exception. + m = load_morphology(NEURON_PATH) + dm.Dendrogram(m) + assert len(get('section_path_distances', m)) > 0 diff --git a/tests/view/test_view.py b/tests/view/test_matplotlib_impl.py similarity index 60% rename from tests/view/test_view.py rename to tests/view/test_matplotlib_impl.py index 94ed37d56..c191a59e5 100644 --- a/tests/view/test_view.py +++ b/tests/view/test_matplotlib_impl.py @@ -30,18 +30,15 @@ from pathlib import Path import numpy as np -from neurom import load_neuron +from neurom import load_morphology from neurom.core.types import NeuriteType -from neurom.view import common, view +from neurom.view import matplotlib_utils, matplotlib_impl import pytest from numpy.testing import assert_allclose, assert_array_almost_equal DATA_PATH = Path(__file__).parent.parent / 'data' SWC_PATH = DATA_PATH / 'swc' -fst_neuron = load_neuron(SWC_PATH / 'Neuron.swc') -simple_neuron = load_neuron(SWC_PATH / 'simple.swc') -neuron_different = load_neuron(SWC_PATH / 'simple-different-section-types.swc') tree_colors = {'black': np.array([[0., 0., 0., 1.] for _ in range(3)]), None: [[1., 0., 0., 1.], [1., 0., 0., 1.], @@ -49,10 +46,11 @@ def test_tree_diameter_scale(get_fig_2d): + m = load_morphology(SWC_PATH / 'simple-different-section-types.swc') fig, ax = get_fig_2d - tree = neuron_different.neurites[0] + tree = m.neurites[0] for input_color, expected_colors in tree_colors.items(): - view.plot_tree(ax, tree, color=input_color, diameter_scale=None, alpha=1., linewidth=1.2) + matplotlib_impl.plot_tree(tree, ax, color=input_color, diameter_scale=None, alpha=1., linewidth=1.2) collection = ax.collections[0] assert collection.get_linewidth()[0] == 1.2 assert_array_almost_equal(collection.get_colors(), expected_colors) @@ -60,10 +58,11 @@ def test_tree_diameter_scale(get_fig_2d): def test_tree_diameter_real(get_fig_2d): + m = load_morphology(SWC_PATH / 'simple-different-section-types.swc') fig, ax = get_fig_2d - tree = neuron_different.neurites[0] + tree = m.neurites[0] for input_color, expected_colors in tree_colors.items(): - view.plot_tree(ax, tree, color=input_color, alpha=1., linewidth=1.2, realistic_diameters=True) + matplotlib_impl.plot_tree(tree, ax, color=input_color, alpha=1., linewidth=1.2, realistic_diameters=True) collection = ax.collections[0] assert collection.get_linewidth()[0] == 1.0 assert_array_almost_equal(collection.get_facecolors(), expected_colors) @@ -71,43 +70,48 @@ def test_tree_diameter_real(get_fig_2d): def test_tree_invalid(get_fig_2d): + m = load_morphology(SWC_PATH / 'simple-different-section-types.swc') fig, ax = get_fig_2d with pytest.raises(AssertionError): - view.plot_tree(ax, neuron_different.neurites[0], plane='wrong') + matplotlib_impl.plot_tree(m.neurites[0], ax=ax, plane='wrong') def test_tree_bounds(get_fig_2d): + m = load_morphology(SWC_PATH / 'simple-different-section-types.swc') fig, ax = get_fig_2d - view.plot_tree(ax, neuron_different.neurites[0]) + matplotlib_impl.plot_tree(m.neurites[0], ax=ax) np.testing.assert_allclose(ax.dataLim.bounds, (-5., 0., 11., 5.)) -def test_neuron(get_fig_2d): +def test_morph(get_fig_2d): + m = load_morphology(SWC_PATH / 'Neuron.swc') fig, ax = get_fig_2d - view.plot_neuron(ax, fst_neuron) - assert ax.get_title() == fst_neuron.name + matplotlib_impl.plot_morph(m, ax=ax) + assert ax.get_title() == m.name assert_allclose(ax.dataLim.get_points(), [[-40.32853516, -57.600172], [64.74726272, 48.51626225], ]) with pytest.raises(AssertionError): - view.plot_tree(ax, fst_neuron, plane='wrong') + matplotlib_impl.plot_tree(m, ax, plane='wrong') def test_tree3d(get_fig_3d): + m = load_morphology(SWC_PATH / 'simple.swc') fig, ax = get_fig_3d - tree = simple_neuron.neurites[0] - view.plot_tree3d(ax, tree) + tree = m.neurites[0] + matplotlib_impl.plot_tree3d(tree, ax) xy_bounds = ax.xy_dataLim.bounds np.testing.assert_allclose(xy_bounds, (-5., 0., 11., 5.)) zz_bounds = ax.zz_dataLim.bounds np.testing.assert_allclose(zz_bounds, (0., 0., 1., 1.)) -def test_neuron3d(get_fig_3d): +def test_morph3d(get_fig_3d): + m = load_morphology(SWC_PATH / 'Neuron.swc') fig, ax = get_fig_3d - view.plot_neuron3d(ax, fst_neuron) - assert ax.get_title() == fst_neuron.name + matplotlib_impl.plot_morph3d(m, ax) + assert ax.get_title() == m.name assert_allclose(ax.xy_dataLim.get_points(), [[-40.32853516, -57.600172], [64.74726272, 48.51626225], ]) @@ -115,46 +119,44 @@ def test_neuron3d(get_fig_3d): (-00.09999862, 54.20408797)) -def test_neuron_no_neurites(get_fig_2d): +def test_morph_no_neurites(): filename = Path(SWC_PATH, 'point_soma.swc') - fig, ax = get_fig_2d - view.plot_neuron(ax, load_neuron(filename)) + matplotlib_impl.plot_morph(load_morphology(filename)) -def test_neuron3d_no_neurites(get_fig_3d): +def test_morph3d_no_neurites(): filename = Path(SWC_PATH, 'point_soma.swc') - fig, ax = get_fig_3d - view.plot_neuron3d(ax, load_neuron(filename)) + matplotlib_impl.plot_morph3d(load_morphology(filename)) def test_dendrogram(get_fig_2d): + m = load_morphology(SWC_PATH / 'Neuron.swc') fig, ax = get_fig_2d - view.plot_dendrogram(ax, fst_neuron) + matplotlib_impl.plot_dendrogram(m, ax) assert_allclose(ax.get_xlim(), (-10., 180.), rtol=0.25) - view.plot_dendrogram(ax, fst_neuron, show_diameters=False) + matplotlib_impl.plot_dendrogram(m, ax, show_diameters=False) assert_allclose(ax.get_xlim(), (-10., 180.), rtol=0.25) + matplotlib_impl.plot_dendrogram(m.neurites[0], ax, show_diameters=False) -with warnings.catch_warnings(record=True): - soma0 = fst_neuron.soma + m = load_morphology(SWC_PATH / 'empty_segments.swc') + matplotlib_impl.plot_dendrogram(m, ax) - # upright, varying radius - soma_2pt_normal = load_neuron(StringIO(u"""1 1 0 0 0 1 -1 - 2 1 0 10 0 10 1"""), reader='swc').soma +with warnings.catch_warnings(record=True): # upright, uniform radius, multiple cylinders - soma_3pt_normal = load_neuron(StringIO(u"""1 1 0 -10 0 10 -1 + soma_3pt_normal = load_morphology(StringIO(u"""1 1 0 -10 0 10 -1 2 1 0 0 0 10 1 3 1 0 10 0 10 2"""), reader='swc').soma # increasing radius, multiple cylinders - soma_4pt_normal_cylinder = load_neuron(StringIO(u"""1 1 0 0 0 1 -1 + soma_4pt_normal_cylinder = load_morphology(StringIO(u"""1 1 0 0 0 1 -1 2 1 0 -10 0 2 1 3 1 0 -10 10 4 2 4 1 -10 -10 -10 4 3"""), reader='swc').soma - soma_4pt_normal_contour = load_neuron(StringIO(u"""((CellBody) + soma_4pt_normal_contour = load_morphology(StringIO(u"""((CellBody) (0 0 0 1) (0 -10 0 2) (0 -10 10 4) @@ -162,32 +164,49 @@ def test_dendrogram(get_fig_2d): def test_soma(get_fig_2d): + m = load_morphology(SWC_PATH / 'Neuron.swc') + soma0 = m.soma fig, ax = get_fig_2d for s in (soma0, soma_3pt_normal, soma_4pt_normal_cylinder, soma_4pt_normal_contour): - view.plot_soma(ax, s) - common.plt.close(fig) + matplotlib_impl.plot_soma(s, ax) + matplotlib_utils.plt.close(fig) - view.plot_soma(ax, s, soma_outline=False) - common.plt.close(fig) + matplotlib_impl.plot_soma(s, ax, soma_outline=False) + matplotlib_utils.plt.close(fig) def test_soma3d(get_fig_3d): _, ax = get_fig_3d - view.plot_soma3d(ax, soma_3pt_normal) + matplotlib_impl.plot_soma3d(soma_3pt_normal, ax) assert_allclose(ax.get_xlim(), (-11., 11.), atol=2) assert_allclose(ax.get_ylim(), (-11., 11.), atol=2) assert_allclose(ax.get_zlim(), (-10., 10.), atol=2) def test_get_color(): - assert view._get_color(None, NeuriteType.basal_dendrite) == "red" - assert view._get_color(None, NeuriteType.axon) == "blue" - assert view._get_color(None, NeuriteType.apical_dendrite) == "purple" - assert view._get_color(None, NeuriteType.soma) == "black" - assert view._get_color(None, NeuriteType.undefined) == "green" - assert view._get_color(None, 'wrong') == "green" - assert view._get_color('blue', 'wrong') == "blue" - assert view._get_color('yellow', NeuriteType.axon) == "yellow" + assert matplotlib_impl._get_color(None, NeuriteType.basal_dendrite) == "red" + assert matplotlib_impl._get_color(None, NeuriteType.axon) == "blue" + assert matplotlib_impl._get_color(None, NeuriteType.apical_dendrite) == "purple" + assert matplotlib_impl._get_color(None, NeuriteType.soma) == "black" + assert matplotlib_impl._get_color(None, NeuriteType.undefined) == "green" + assert matplotlib_impl._get_color(None, 'wrong') == "green" + assert matplotlib_impl._get_color('blue', 'wrong') == "blue" + assert matplotlib_impl._get_color('yellow', NeuriteType.axon) == "yellow" + + +def test_filter_neurite(): + m = load_morphology(SWC_PATH / 'Neuron.swc') + fig, ax = matplotlib_utils.get_figure(params={'projection': '3d'}) + matplotlib_impl.plot_morph3d(m, ax, neurite_type=NeuriteType.basal_dendrite) + matplotlib_utils.plot_style(fig=fig, ax=ax) + assert_allclose(matplotlib_utils.plt.gca().get_ylim(), [-30., 78], atol=5) + matplotlib_utils.plt.close('all') + + fig, ax = matplotlib_utils.get_figure() + matplotlib_impl.plot_morph(m, ax, neurite_type=NeuriteType.basal_dendrite) + matplotlib_utils.plot_style(fig=fig, ax=ax) + assert_allclose(matplotlib_utils.plt.gca().get_ylim(), [-30., 78], atol=5) + matplotlib_utils.plt.close('all') diff --git a/tests/view/test_common.py b/tests/view/test_matplotlib_utils.py similarity index 96% rename from tests/view/test_common.py rename to tests/view/test_matplotlib_utils.py index face543bd..8afa57ede 100644 --- a/tests/view/test_common.py +++ b/tests/view/test_matplotlib_utils.py @@ -29,9 +29,9 @@ import tempfile import numpy as np -from neurom.view.common import (plt, figure_naming, get_figure, save_plot, plot_style, - plot_title, plot_labels, plot_legend, update_plot_limits, plot_ticks, - plot_sphere, plot_cylinder) +from neurom.view.matplotlib_utils import (plt, figure_naming, get_figure, save_plot, plot_style, + plot_title, plot_labels, plot_legend, update_plot_limits, plot_ticks, + plot_sphere, plot_cylinder) import pytest diff --git a/tests/view/test_plotly_impl.py b/tests/view/test_plotly_impl.py new file mode 100644 index 000000000..ba58d8627 --- /dev/null +++ b/tests/view/test_plotly_impl.py @@ -0,0 +1,50 @@ +import sys +from pathlib import Path + +import neurom +from neurom import load_morphology +from neurom.view import plotly_impl + +import mock +from numpy.testing import assert_allclose + +SWC_PATH = Path(__file__).parent.parent / 'data/swc' +MORPH_FILENAME = SWC_PATH / 'Neuron.swc' +m = load_morphology(MORPH_FILENAME) + + +def _reload_module(module): + """Force module reload.""" + import importlib + importlib.reload(module) + + +def test_plotly_extra_not_installed(): + with mock.patch.dict(sys.modules, {'plotly': None}): + try: + _reload_module(neurom.view.plotly_impl) + assert False, "ImportError not triggered" + except ImportError as e: + assert (str(e) == + 'neurom[plotly] is not installed. ' + 'Please install it by doing: pip install neurom[plotly]') + + +def test_plotly_draw_morph3d(): + plotly_impl.plot_morph3d(m, auto_open=False) + plotly_impl.plot_morph3d(m.neurites[0], auto_open=False) + + fig = plotly_impl.plot_morph3d(load_morphology(SWC_PATH / 'simple-different-soma.swc'), + auto_open=False) + x, y, z = [fig['data'][2][key] for key in str('xyz')] + assert_allclose(x[0, 0], 2) + assert_allclose(x[33, 33], -1.8971143170299758) + assert_allclose(y[0, 0], 3) + assert_allclose(y[33, 33], 9.75) + assert_allclose(z[0, 0], 13) + assert_allclose(z[33, 33], 8.5) + + +def test_plotly_draw_morph2d(): + plotly_impl.plot_morph(m, auto_open=False) + plotly_impl.plot_morph(m.neurites[0], auto_open=False) diff --git a/tutorial/getting_started.ipynb b/tutorial/getting_started.ipynb index ab569fd5d..c1bf57c6d 100644 --- a/tutorial/getting_started.ipynb +++ b/tutorial/getting_started.ipynb @@ -30,8 +30,7 @@ "# Import neurom module\n", "import neurom as nm\n", "# Import neurom visualization module\n", - "from neurom import viewer\n", - "from neurom.view import plotly" + "from neurom.view import matplotlib_impl, matplotlib_utils, plotly_impl" ] }, { @@ -50,13 +49,10 @@ "outputs": [], "source": [ "# Load a single morphology \n", - "neuron = nm.load_neuron('../tests/data/valid_set/Neuron.swc')\n", + "neuron = nm.load_morphology('../tests/data/valid_set/Neuron.swc')\n", "\n", "# Load a population of morphologies from a set of files\n", - "pop = nm.load_neurons('../tests/data/valid_set/')\n", - "\n", - "# Get a single morphology from the population\n", - "single_neuron = pop.neurons[0]" + "pop = nm.load_morphologies('../tests/data/valid_set/')" ] }, { @@ -73,7 +69,7 @@ "outputs": [], "source": [ "# Visualize a morphology in two dimensions\n", - "fig, ax = plotly.draw(neuron, plane='xy', inline=True)" + "fig, ax = plotly_impl.plot_morph(neuron, plane='xy', inline=True)" ] }, { @@ -83,7 +79,7 @@ "outputs": [], "source": [ "# Visualize a morphology in three dimensions\n", - "fig, ax = plotly.draw(neuron, inline=True)" + "fig, ax = plotly_impl.plot_morph3d(neuron, inline=True)" ] }, { @@ -93,7 +89,7 @@ "outputs": [], "source": [ "# Visualize a single tree in three dimensions\n", - "fig, ax = plotly.draw(neuron.neurites[0], inline=True)" + "fig, ax = plotly_impl.plot_morph3d(neuron.neurites[0], inline=True)" ] }, { @@ -103,7 +99,9 @@ "outputs": [], "source": [ "# Visualize the dendrogram of a morphology\n", - "fig, ax = viewer.draw(neuron, mode='dendrogram')" + "fig, ax = matplotlib_utils.get_figure()\n", + "matplotlib_impl.plot_dendrogram(neuron, ax)\n", + "matplotlib_utils.plot_style(fig=fig, ax=ax)" ] }, { @@ -136,7 +134,7 @@ "number_of_sections_per_neurite = nm.get('number_of_sections_per_neurite', neuron)\n", "\n", "# Print result\n", - "print(\"Neuron id : {0} \\n\\\n", + "print(\"Morphology id : {0} \\n\\\n", "Number of neurites : {1} \\n\\\n", "Soma radius : {2:.2f} \\n\\\n", "Number of sections : {3}\".format(neuron.name, number_of_neurites[0], soma_radius, number_of_sections[0]))\n", @@ -417,6 +415,13 @@ "print(selected_section_lengths)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, diff --git a/tutorial/plotly.ipynb b/tutorial/plotly.ipynb deleted file mode 100644 index 6ae5107ad..000000000 --- a/tutorial/plotly.ipynb +++ /dev/null @@ -1,80 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from neurom.view.plotly import draw\n", - "import neurom\n", - "from pathlib import Path\n", - "\n", - "path = Path(neurom.__file__).parent.parent / 'tests/data/valid_set/Neuron.swc'\n", - "neuron = neurom.load_neuron(path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 3D plot" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "draw(neuron, inline=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 2D plot" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "draw(neuron, plane='xy', inline=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -}