From b6c3beb4839a0dac90aaff7dad9e9219acce8f51 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 25 Feb 2022 10:43:15 +0100 Subject: [PATCH 01/87] Use soma center in sholl_frequency distance calculation (#989) Calculate the distance of the morphology points to the soma, instead to (0,0,0) --- neurom/features/morphology.py | 6 ++++-- tests/features/test_get_features.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 2315e127..67583e92 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -478,8 +478,10 @@ def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None 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)) + max_radii = max( + np.max(np.linalg.norm(n.points[:, COLS.XYZ] - morph.soma.center, 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, neurite_type, morph.soma.center, bins) diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index 6f20106a..312edaf4 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -706,6 +706,17 @@ def test_sholl_frequency(): assert len(features.get('sholl_frequency', POP)) == 108 + # check that the soma is taken into account for calculating max radius and num bins + m = nm.load_morphology( + """ + 1 1 -10 0 0 5.0 -1 + 2 3 0 0 0 0.1 1 + 3 3 10 0 0 0.1 2 + """, reader="swc", + ) + + assert features.get('sholl_frequency', m, step_size=5.0) == [0, 1, 1, 1] + def test_bifurcation_partitions(): assert_allclose(features.get('bifurcation_partitions', POP)[:10], [19., 17., 15., 13., 11., 9., 7., 5., 3., 1.]) From d120263701bd2de8fb6f8ea6c655b9ec8a0a99ce Mon Sep 17 00:00:00 2001 From: Adrien Berchet Date: Mon, 28 Feb 2022 09:25:31 +0100 Subject: [PATCH 02/87] Fix the trunk_origin_radii feature (#986) The feature throws a warning if path distances deeper than the first section are used and handles all special cases wrt the combinations of min/max filters. --- neurom/features/morphology.py | 50 +++++++++++++++++++-- tests/features/test_morphology.py | 74 +++++++++++++++++++++++++++---- 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 67583e92..405816d9 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -43,6 +43,7 @@ For more details see :ref:`features`. """ +import warnings from functools import partial import math @@ -53,6 +54,7 @@ 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.exceptions import NeuroMError from neurom.features import feature, NameSpace, neurite as nf from neurom.utils import str_to_plane @@ -356,6 +358,8 @@ def trunk_origin_radii( .. warning:: If ``min_length_filter`` and / or ``max_length_filter`` is given, the points are filtered and the mean radii of the remaining points is returned. + Note that if the ``min_length_filter`` is greater than the path distance of the last point + of the first section, the radius of this last point is returned. Args: morph: The morphology to process. @@ -374,18 +378,56 @@ def trunk_origin_radii( return [n.root_node.points[0][COLS.R] for n in iter_neurites(morph, filt=is_type(neurite_type))] - def _mean_radius(points): + if min_length_filter is not None and min_length_filter <= 0: + raise NeuroMError( + "In 'trunk_origin_radii': the 'min_length_filter' value must be strictly greater " + "than 0." + ) + + if max_length_filter is not None and max_length_filter <= 0: + raise NeuroMError( + "In 'trunk_origin_radii': the 'max_length_filter' value must be strictly greater " + "than 0." + ) + + if ( + min_length_filter is not None + and max_length_filter is not None + and min_length_filter >= max_length_filter + ): + raise NeuroMError( + "In 'trunk_origin_radii': the 'min_length_filter' value must be strictly less than the " + "'max_length_filter' value." + ) + + def _mean_radius(neurite): + points = neurite.root_node.points interval_lengths = morphmath.interval_lengths(points) path_lengths = np.insert(np.cumsum(interval_lengths), 0, 0) valid_pts = np.ones(len(path_lengths), dtype=bool) if min_length_filter is not None: valid_pts = (valid_pts & (path_lengths >= min_length_filter)) + if not valid_pts.any(): + warnings.warn( + "In 'trunk_origin_radii': the 'min_length_filter' value is greater than the " + "path distance of the last point of the last section so the radius of this " + "point is returned." + ) + return points[-1, COLS.R] if max_length_filter is not None: - valid_pts = (valid_pts & (path_lengths <= max_length_filter)) + valid_max = (path_lengths <= max_length_filter) + valid_pts = (valid_pts & valid_max) + if not valid_pts.any(): + warnings.warn( + "In 'trunk_origin_radii': the 'min_length_filter' and 'max_length_filter' " + "values excluded all the points of the section so the radius of the first " + "point after the 'min_length_filter' path distance is returned." + ) + # pylint: disable=invalid-unary-operand-type + return points[~valid_max, COLS.R][0] return points[valid_pts, COLS.R].mean() - return [_mean_radius(n.points) - for n in iter_neurites(morph, filt=is_type(neurite_type))] + return [_mean_radius(n) for n in iter_neurites(morph, filt=is_type(neurite_type))] @feature(shape=(...,)) diff --git a/tests/features/test_morphology.py b/tests/features/test_morphology.py index a918854e..565b856d 100644 --- a/tests/features/test_morphology.py +++ b/tests/features/test_morphology.py @@ -43,6 +43,8 @@ from neurom import morphmath from neurom import NeuriteType, load_morphology, AXON, BASAL_DENDRITE +from neurom.core import Morphology +from neurom.exceptions import NeuroMError from neurom.features import morphology, section @@ -151,17 +153,71 @@ def test_trunk_section_lengths(): def test_trunk_origin_radii(): - ret = morphology.trunk_origin_radii(SIMPLE) - assert ret == [1.0, 1.0] - - ret = morphology.trunk_origin_radii(SIMPLE, min_length_filter=5) - assert_array_almost_equal(ret, [1.0 / 3, 0.0]) + morph = Morphology(SIMPLE) + morph.section(0).diameters = [2, 1] + morph.section(3).diameters = [2, 0.5] - ret = morphology.trunk_origin_radii(SIMPLE, max_length_filter=15) - assert_array_almost_equal(ret, [2.0 / 3, 2.0 / 3]) + ret = morphology.trunk_origin_radii(morph) + assert ret == [1.0, 1.0] - ret = morphology.trunk_origin_radii(SIMPLE, min_length_filter=5, max_length_filter=15) - assert_array_almost_equal(ret, [0.5, 0]) + ret = morphology.trunk_origin_radii(morph, min_length_filter=1) + assert_array_almost_equal(ret, [0.5, 0.25]) + + with pytest.warns( + UserWarning, + match=( + r"In 'trunk_origin_radii': the 'min_length_filter' value is greater than the " + r"path distance of the last point of the last section so the radius of this " + r"point is returned\." + ) + ): + ret = morphology.trunk_origin_radii(morph, min_length_filter=999) + assert_array_almost_equal(ret, [0.5, 0.25]) + + ret = morphology.trunk_origin_radii(morph, max_length_filter=15) + assert_array_almost_equal(ret, [3.0 / 4, 5.0 / 8]) + + ret = morphology.trunk_origin_radii(morph, min_length_filter=1, max_length_filter=15) + assert_array_almost_equal(ret, [0.5, 0.25]) + + with pytest.warns( + UserWarning, + match=( + r"In 'trunk_origin_radii': the 'min_length_filter' and 'max_length_filter' " + r"values excluded all the points of the section so the radius of the first " + r"point after the 'min_length_filter' path distance is returned\." + ) + ): + ret = morphology.trunk_origin_radii(morph, min_length_filter=0.1, max_length_filter=0.2) + assert_array_almost_equal(ret, [0.5, 0.25]) + + with pytest.raises( + NeuroMError, + match=( + r"In 'trunk_origin_radii': the 'min_length_filter' value must be strictly greater " + r"than 0\." + + ) + ): + ret = morphology.trunk_origin_radii(morph, min_length_filter=-999) + + with pytest.raises( + NeuroMError, + match=( + r"In 'trunk_origin_radii': the 'max_length_filter' value must be strictly greater " + r"than 0\." + ) + ): + ret = morphology.trunk_origin_radii(morph, max_length_filter=-999) + + with pytest.raises( + NeuroMError, + match=( + r"In 'trunk_origin_radii': the 'min_length_filter' value must be strictly less than the" + r" 'max_length_filter' value\." + ) + ): + ret = morphology.trunk_origin_radii(morph, min_length_filter=15, max_length_filter=5) def test_trunk_origin_azimuths(): From d89665167bb540c25cc1794ff9396b20138ccc67 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 28 Feb 2022 11:50:28 +0100 Subject: [PATCH 03/87] Fix `sholl_frequency` for NA neurite_type (#990) Make sholl_frequency return an empty list for a neurite_type that is not present in the morphology. --- CHANGELOG.rst | 8 ++++++++ neurom/features/morphology.py | 14 +++++++++++--- tests/features/test_get_features.py | 4 ++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f852163d..de024a1e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,10 +3,18 @@ Changelog Version 3.2.0 ------------- +- Fix ``neurom.features.morphology.sholl_frequency`` to return an empty list when a + neurite_type that is not present in the morphology is specified. +- Fix ``neurom.features.morphology.trunk_origin_radii`` to warn and use only the root + section for the calculation of the path distances. Edge cases from the combination + of ``min_length_filter`` and ``max_length_filter`` are addressed. +- Fix ``neurom.features.morphology.sholl_frequency`` to use soma center in distance + calculation, instead of using the origin. - Add ``neurom.features.morphology.trunk_angles_inter_types`` and ``neurom.features.morphology.trunk_angles_from_vector`` features, make ``neurom.features.morphology.trunk_angles`` more generic and add length filter to ``neurom.features.morphology.trunk_origin_radii``. +- Deprecate python3.6 - Add doc on spherical coordinates. Version 3.1.0 diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 405816d9..2aded0ee 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -515,16 +515,24 @@ def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None 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. + + If a `neurite_type` is specified and there are no trees corresponding to it, an empty + list will be returned. """ neurite_filter = is_type(neurite_type) if bins is None: min_soma_edge = morph.soma.radius - max_radii = max( + + max_radius_per_neurite = [ np.max(np.linalg.norm(n.points[:, COLS.XYZ] - morph.soma.center, axis=1)) for n in morph.neurites if neurite_filter(n) - ) - bins = np.arange(min_soma_edge, min_soma_edge + max_radii, step_size) + ] + + if not max_radius_per_neurite: + return [] + + bins = np.arange(min_soma_edge, min_soma_edge + max(max_radius_per_neurite), step_size) return sholl_crossings(morph, neurite_type, morph.soma.center, bins) diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index 312edaf4..61d31191 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -717,6 +717,10 @@ def test_sholl_frequency(): assert features.get('sholl_frequency', m, step_size=5.0) == [0, 1, 1, 1] + # check that if there is no neurite of a specific type, an empty list is returned + assert features.get('sholl_frequency', m, neurite_type=NeuriteType.axon) == [] + + def test_bifurcation_partitions(): assert_allclose(features.get('bifurcation_partitions', POP)[:10], [19., 17., 15., 13., 11., 9., 7., 5., 3., 1.]) From e071ba593092254698c44614d4f6d8a081ffa5f6 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 3 Mar 2022 14:28:24 +0100 Subject: [PATCH 04/87] Add py39 and py310 for tests (#984) --- .github/workflows/run-tox.yml | 2 +- tox.ini | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tox.yml b/.github/workflows/run-tox.yml index aeb8f1ec..ee7782ff 100644 --- a/.github/workflows/run-tox.yml +++ b/.github/workflows/run-tox.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 diff --git a/tox.ini b/tox.ini index 8671e758..dd28e9a2 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ testdeps = [tox] envlist = - {py37,py38} + py{37,38,39,310} py38-lint py38-coverage py38-docs @@ -60,3 +60,5 @@ convention = google python = 3.7: py37 3.8: py38 + 3.9: py39 + 3.10: py310 From 0b7b5ea89d5857e9dda2783dbd62736e05df6a57 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 4 Mar 2022 09:22:59 +0100 Subject: [PATCH 05/87] Make output file in morph_check required (#992) --- neurom/apps/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neurom/apps/cli.py b/neurom/apps/cli.py index 8b2c8a78..845d66df 100644 --- a/neurom/apps/cli.py +++ b/neurom/apps/cli.py @@ -107,7 +107,7 @@ def stats(datapath, config, output, full_config, as_population, ignored_exceptio default=morph_check.EXAMPLE_CONFIG, show_default=True, help='Configuration File') @click.option('-o', '--output', type=click.Path(exists=False, dir_okay=False), - help='Path to output json summary file') + help='Path to output json summary file', required=True) def check(datapath, config, output): """Cli for apps/morph_check.""" morph_check.main(datapath, config, output) From 8c3efc8da9c996db19a8aba1382e82de97c786a0 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 7 Mar 2022 08:55:09 +0100 Subject: [PATCH 06/87] Minor housekeeping (#993) * Use the standard pyproject.toml build-system * Update publish-sdist with python3.9 * Set min python version to 3.7 --- .github/workflows/publish-sdist.yml | 4 ++-- pyproject.toml | 6 +++++- setup.py | 5 +++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-sdist.yml b/.github/workflows/publish-sdist.yml index 466fb4e2..eea174c4 100644 --- a/.github/workflows/publish-sdist.yml +++ b/.github/workflows/publish-sdist.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - - name: Set up Python 3.6 + - name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: 3.9 - name: Build a source tarball run: python setup.py sdist diff --git a/pyproject.toml b/pyproject.toml index d1e6ae6e..f6c16894 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,6 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = [ + "setuptools>=42", + "wheel", +] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index f1290812..6c0cbf07 100644 --- a/setup.py +++ b/setup.py @@ -57,15 +57,16 @@ 'docs': ['sphinx', 'sphinx-bluebrain-theme', 'sphinx-autorun'], }, include_package_data=True, - python_requires='>=3.5', + python_requires='>=3.7', classifiers=[ 'Development Status :: 6 - Mature', 'Intended Audience :: Education', 'Intended Audience :: Science/Research', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Topic :: Scientific/Engineering :: Bio-Informatics', ], use_scm_version={"local_scheme": "no-local-version"}, From c570a2faa18f4e417fcdb743f6487beee0c60c3e Mon Sep 17 00:00:00 2001 From: Adrien Berchet Date: Tue, 8 Mar 2022 11:42:31 +0100 Subject: [PATCH 07/87] Fix doc of neurom.core.morphology.Neurite.iter_sections method (#994) Thanks! --- neurom/core/morphology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 077cb6b7..0601c418 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -390,7 +390,7 @@ def iter_sections(self, order=Section.ipreorder, neurite_order=NeuriteIter.FileO 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.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 From cfc7012359a0faf34bc0dfdf9c900a2b910e0021 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 10 Mar 2022 08:42:28 +0100 Subject: [PATCH 08/87] Add volume density at the morphology level (#996) --- neurom/features/morphology.py | 34 +++++++++++++- neurom/features/neurite.py | 11 +---- neurom/geom/__init__.py | 28 ++++++++++-- tests/features/test_get_features.py | 16 +++++++ tests/features/test_morphology.py | 71 +++++++++++++++++++++++++++++ tests/features/test_neurite.py | 18 ++++++-- tests/geom/test_geom.py | 10 +++- 7 files changed, 167 insertions(+), 21 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 2aded0ee..3159aa69 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -57,6 +57,7 @@ from neurom.exceptions import NeuroMError from neurom.features import feature, NameSpace, neurite as nf from neurom.utils import str_to_plane +from neurom.geom import convex_hull feature = partial(feature, namespace=NameSpace.NEURON) @@ -276,7 +277,7 @@ def trunk_angles_inter_types( if len(source_vectors) == 0 or len(target_vectors) == 0: return [] - angles = np.empty((len(source_vectors), len(target_vectors), 3), dtype=np.float) + angles = np.empty((len(source_vectors), len(target_vectors), 3), dtype=float) for i, source in enumerate(source_vectors): for j, target in enumerate(target_vectors): @@ -571,3 +572,34 @@ def total_height(morph, neurite_type=NeuriteType.all): def total_depth(morph, neurite_type=NeuriteType.all): """Extent of morphology along axis z.""" return _extent_along_axis(morph, axis=COLS.Z, neurite_type=neurite_type) + + +@feature(shape=()) +def volume_density(morph, neurite_type=NeuriteType.all): + """Get the volume density. + + The volume density is defined as the ratio of the neurite volume and + the volume of the morphology's enclosing convex hull + + .. note:: Returns `np.nan` if the convex hull computation fails or there are not points + available due to neurite type filtering. + """ + + def get_points(neurite): + return neurite.points[:, COLS.XYZ] + + # note: duplicate points are present but do not affect convex hull calculation + points = [ + point + for point_list in iter_neurites(morph, mapfun=get_points, filt=is_type(neurite_type)) + for point in point_list + ] + + morph_hull = convex_hull(points) + + if morph_hull is None: + return np.nan + + total_volume = sum(iter_neurites(morph, mapfun=nf.total_volume, filt=is_type(neurite_type))) + + return total_volume / morph_hull.volume diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index b6b6d565..0ce9db3e 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -48,7 +48,6 @@ 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 @@ -457,14 +456,8 @@ def volume_density(neurite): .. 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 + neurite_hull = convex_hull(neurite.points[:, COLS.XYZ]) + return neurite.volume / neurite_hull.volume if neurite_hull is not None else np.nan @feature(shape=(...,)) diff --git a/neurom/geom/__init__.py b/neurom/geom/__init__.py index 3e810793..f23f865c 100644 --- a/neurom/geom/__init__.py +++ b/neurom/geom/__init__.py @@ -28,12 +28,18 @@ """Geometrical Operations for NeuroM.""" +import logging + import numpy as np from scipy.spatial import ConvexHull +from scipy.spatial.qhull import QhullError from neurom.core.dataformat import COLS from neurom.geom.transform import translate, rotate +L = logging.getLogger(__name__) + + def bounding_box(obj): """Get the (x, y, z) bounding box of an object containing points. @@ -44,10 +50,24 @@ def bounding_box(obj): np.max(obj.points[:, COLS.XYZ], axis=0)]) -def convex_hull(obj): - """Get the convex hull of an object containing points. +def convex_hull(point_data): + """Get the convex hull from point data. Returns: - scipy.spatial.ConvexHull object built from obj.points + scipy.spatial.ConvexHull object if successful, otherwise None """ - return ConvexHull(obj.points[:, COLS.XYZ]) + if len(point_data) == 0: + L.exception( + "Failure to compute convex hull because there are no points" + ) + return None + + points = np.asarray(point_data)[:, COLS.XYZ] + + try: + return ConvexHull(points) + except QhullError: + L.exception( + "Failure to compute convex hull because points like on a 2D plane." + ) + return None diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index 61d31191..edc7eb58 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -486,6 +486,22 @@ def test_neurite_density(): (0.24068543213643726, 0.52464681266899216, 0.76533224480542938, 0.38266612240271469)) +def test_morphology_volume_density(): + + volume_density = features.get("volume_density", NEURON) + + # volume density should not be calculated as the sum of the neurite volume densities, + # because it is not additive + volume_density_from_neurites = sum( + features.get("volume_density", neu) for neu in NEURON.neurites + ) + + # calculating the convex hull per neurite results into smaller hull volumes and higher + # neurite_volume / hull_volume ratios + assert not np.isclose(volume_density, volume_density_from_neurites) + assert volume_density < volume_density_from_neurites + + def test_section_lengths(): ref_seclen = [n.length for n in iter_sections(NEURON)] seclen = features.get('section_lengths', NEURON) diff --git a/tests/features/test_morphology.py b/tests/features/test_morphology.py index 565b856d..59b4cf11 100644 --- a/tests/features/test_morphology.py +++ b/tests/features/test_morphology.py @@ -594,3 +594,74 @@ def test_total_depth(): assert_almost_equal(morphology.total_depth(morph, neurite_type=NeuriteType.axon), 0.0) assert_almost_equal(morphology.total_depth(morph, neurite_type=NeuriteType.basal_dendrite), 2.0) assert_almost_equal(morphology.total_depth(morph, neurite_type=NeuriteType.apical_dendrite), 3.0) + + +def test_volume_density(): + + morph = load_swc(""" + 1 1 0.5 0.5 0.5 0.5 -1 + 2 3 0.211324 0.211324 0.788675 0.1 1 + 3 3 0.0 0.0 1.0 0.1 2 + 4 3 0.211324 0.788675 0.788675 0.1 1 + 5 3 0.0 1.0 1.0 0.1 4 + 6 3 0.788675 0.211324 0.788675 0.1 1 + 7 3 1.0 0.0 1.0 0.1 6 + 8 3 0.211324 0.211324 0.211324 0.1 1 + 9 3 0.0 0.0 0.0 0.1 8 + 10 3 0.211324 0.788675 0.211324 0.1 1 + 11 3 0.0 1.0 0.0 0.1 10 + 12 5 0.788675 0.788675 0.211324 0.1 1 + 13 5 1.0 1.0 0.0 0.1 12 + 14 2 0.788675 0.211324 0.211324 0.1 1 + 15 2 1.0 0.0 0.0 0.1 14 + 16 3 0.788675 0.788675 0.788675 0.1 1 + 17 3 1.0 1.0 1.0 0.1 16 + """) + + # the neurites sprout from the center of a cube to its vertices, therefore the convex hull + # is the cube itself of side 1.0 + expected_hull_volume = 1.0 + + # diagonal - radius + expected_neurite_length = np.sqrt(3) * 0.5 - 0.5 + + # distance from center of unit cube to its vertices is sqrt(3) + expected_neurite_volume = np.pi * 0.1**2 * expected_neurite_length * 8 + + expected_volume_density = expected_neurite_volume / expected_hull_volume + + assert_almost_equal( + morphology.volume_density(morph), + expected_volume_density, + decimal=5 + ) + assert_almost_equal( + morphology.volume_density(morph, neurite_type=NeuriteType.all), + expected_volume_density, + decimal=5 + ) + + # (0 0 1) (0 1 1) (0 0 0) (0 1 0) (1 0 1)(1 1 1) + # form a triangular prism + # Volume = triangle_area * depth = 0.5 * 1. * 1. * 1. + expected_hull_volume = 0.5 + + expected_neurite_volume = np.pi * 0.1**2 * expected_neurite_length * 6 + + expected_volume_density = expected_neurite_volume / expected_hull_volume + + assert_almost_equal( + morphology.volume_density(morph, neurite_type=NeuriteType.basal_dendrite), + expected_volume_density, + decimal=5 + ) + + # invalid convex hull + assert np.isnan( + morphology.volume_density(morph, neurite_type=NeuriteType.axon), + ) + + # no points + assert np.isnan( + morphology.volume_density(morph, neurite_type=NeuriteType.apical_dendrite), + ) diff --git a/tests/features/test_neurite.py b/tests/features/test_neurite.py index 07d9422b..795549ef 100644 --- a/tests/features/test_neurite.py +++ b/tests/features/test_neurite.py @@ -65,7 +65,7 @@ def test_number_of_leaves(): 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)]) + hull_vol = np.array([convex_hull(n.points).volume for n in nm.iter_neurites(NRN)]) vol_density = [neurite.volume_density(s) for s in NRN.neurites] assert len(vol_density) == 4 @@ -77,10 +77,18 @@ def test_neurite_volume_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 + + flat_neuron = nm.load_morphology( + """ + 1 1 0 0 0 0.5 -1 + 2 3 1 0 0 0.1 1 + 3 3 2 0 0 0.1 2 + """, + reader="swc") + + assert np.isnan( + neurite.volume_density(flat_neuron.neurites[0]) + ) def test_terminal_path_length_per_neurite(): diff --git a/tests/geom/test_geom.py b/tests/geom/test_geom.py index 91f6c2bb..f75365c2 100644 --- a/tests/geom/test_geom.py +++ b/tests/geom/test_geom.py @@ -31,6 +31,7 @@ import neurom as nm import numpy as np from neurom import geom +from neurom.core.dataformat import COLS from numpy.testing import assert_almost_equal SWC_DATA_PATH = Path(__file__).parent.parent / 'data/swc' @@ -76,7 +77,7 @@ def test_convex_hull_points(): # This leverages scipy ConvexHull and we don't want # to re-test scipy, so simply check that the points are the same. - hull = geom.convex_hull(NRN) + hull = geom.convex_hull(NRN.points[:, COLS.XYZ]) assert np.alltrue(hull.points == NRN.points[:, :3]) @@ -84,5 +85,10 @@ def test_convex_hull_volume(): # This leverages scipy ConvexHull and we don't want # to re-test scipy, so simply regression test the volume - hull = geom.convex_hull(NRN) + hull = geom.convex_hull(NRN.points[:, COLS.XYZ]) assert_almost_equal(hull.volume, 208641, decimal=0) + + +def test_convex_hull_invalid(): + assert geom.convex_hull([]) is None + assert geom.convex_hull([[1., 0., 0.], [1., 0., 0.]]) is None From 127d6fd45167b3cf2ae6d41c51ad241846d082dc Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 10 Mar 2022 09:17:26 +0100 Subject: [PATCH 09/87] Remove duplicate points in principal_direction_extents (#999) --- neurom/morphmath.py | 10 ++++++---- tests/features/test_get_features.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/neurom/morphmath.py b/neurom/morphmath.py index 00c4385f..b01c5163 100644 --- a/neurom/morphmath.py +++ b/neurom/morphmath.py @@ -441,10 +441,10 @@ def segment_taper_rate(seg): def pca(points): """Estimate the principal components of the covariance on the given point cloud. - Input - A numpy array of points of the form ((x1,y1,z1), (x2, y2, z2)...) + Args: + points: A numpy array of points of the form ((x1,y1,z1), (x2, y2, z2)...) - Ouptut + Returns: Eigenvalues and respective eigenvectors """ return np.linalg.eig(np.cov(points.transpose())) @@ -474,8 +474,10 @@ def principal_direction_extent(points): eigs : eigenvalues of the covariance matrix eigv : respective eigenvectors of the covariance matrix """ + # pca can be biased by duplicate points + points = np.unique(points, axis=0) + # center the points around 0.0 - points = np.copy(points) points -= np.mean(points, axis=0) # principal components diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index edc7eb58..6661b89e 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -797,9 +797,15 @@ def test_principal_direction_extents(): # 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_ref = [ + 1672.969491, + 142.437047, + 224.607978, + 415.50613, + 429.830081, + 165.954097, + 346.832825, + ] p = features.get('principal_direction_extents', m) assert_allclose(p, p_ref, rtol=1e-6) From f57feb2f40c287f644b53da671f7b63cf7a8aef9 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 14 Mar 2022 09:28:56 +0100 Subject: [PATCH 10/87] Fix parition_asymmetry Uylings to not throw for bifurcations with leaves (#1001) --- CHANGELOG.rst | 9 +++++++++ neurom/features/bifurcation.py | 13 +++++++------ tests/features/test_bifurcation.py | 3 +-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index de024a1e..4d0b572d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,15 @@ Changelog Version 3.2.0 ------------- + +- Fix ``neurom.features.bifurcation.partition_asymmetry`` Uylings variant to not throw + for bifurcations with leaves. +- Fix ``neurom.features.neurite.principal_direction_extents`` to remove duplicate points + when calculated. +- Add ``neurom.features.morphology.volume_density`` feature so that it is calculated + correctly when the entire morphology is taken into account instead of summing the per + neurite volume densities. +- Add support for py39 and py310 testing. - Fix ``neurom.features.morphology.sholl_frequency`` to return an empty list when a neurite_type that is not present in the morphology is specified. - Fix ``neurom.features.morphology.trunk_origin_radii`` to warn and use only the root diff --git a/neurom/features/bifurcation.py b/neurom/features/bifurcation.py index 65db001e..6c628d15 100644 --- a/neurom/features/bifurcation.py +++ b/neurom/features/bifurcation.py @@ -115,12 +115,13 @@ def partition_asymmetry(bif_point, uylings=False): 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.') + + if n == m == 1: + # By definition the asymmetry A(1, 1) is zero + return 0.0 + + c = 2.0 if uylings else 0.0 + return abs(n - m) / abs(n + m - c) diff --git a/tests/features/test_bifurcation.py b/tests/features/test_bifurcation.py index 530a4c4e..f2eea4c0 100644 --- a/tests/features/test_bifurcation.py +++ b/tests/features/test_bifurcation.py @@ -80,8 +80,7 @@ def test_partition_asymmetry(): assert bf.partition_asymmetry(root) == 0.5 assert bf.partition_asymmetry(root.children[0]) == 0.0 assert bf.partition_asymmetry(root, True) == 1.0 - with pytest.raises(NeuroMError, match='Uylings'): - bf.partition_asymmetry(root.children[0], True) + assert bf.partition_asymmetry(root.children[0], True) == 0.0 leaf = root.children[0].children[0] assert_raises(NeuroMError, bf.partition_asymmetry, leaf) From 79c62e3986c8682a14f7a51cb4b32f5aca4244ca Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 15 Mar 2022 14:34:48 +0100 Subject: [PATCH 11/87] Make deprecation warnings respect pre-existing warnings configuration (#1002) The NeuroM deprecation warning configuration was hijacking the warnings setup in order to enable visibility of the DeprecationWarning. This change doesn't enforce a warning filter configuration, but increases the stack level of the warn function to 3. Setting it to 3 brings the warning stacl level to the __main__ (e.g. user's interactive shell) where the visibility of the DeprecationWarning is by default enabled. --- CHANGELOG.rst | 1 + neurom/__init__.py | 2 +- neurom/exceptions.py | 4 ++++ neurom/utils.py | 5 ++--- tests/test_utils.py | 9 +++++++++ 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d0b572d..21e19fe7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Version 3.2.0 ------------- +- Fix warning system so that it doesn't change the pre-existing warnings configuration - Fix ``neurom.features.bifurcation.partition_asymmetry`` Uylings variant to not throw for bifurcations with leaves. - Fix ``neurom.features.neurite.principal_direction_extents`` to remove duplicate points diff --git a/neurom/__init__.py b/neurom/__init__.py index 90290b56..2b2f24aa 100644 --- a/neurom/__init__.py +++ b/neurom/__init__.py @@ -55,7 +55,6 @@ >>> mapping = lambda n : len(n.points) >>> 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.morphology import graft_morphology, iter_neurites, iter_sections, iter_segments @@ -63,6 +62,7 @@ from neurom.features import get from neurom.io.utils import MorphLoader, load_morphology, load_morphologies from neurom.io.utils import load_neuron, load_neurons +from neurom.exceptions import NeuroMDeprecationWarning APICAL_DENDRITE = NeuriteType.apical_dendrite BASAL_DENDRITE = NeuriteType.basal_dendrite diff --git a/neurom/exceptions.py b/neurom/exceptions.py index fac38b2c..7eba4c88 100644 --- a/neurom/exceptions.py +++ b/neurom/exceptions.py @@ -35,3 +35,7 @@ class NeuroMError(Exception): class ConfigError(NeuroMError): """Exception class for configuration data in apps errors.""" + + +class NeuroMDeprecationWarning(DeprecationWarning): + """NeuroM deprecation warning for users.""" diff --git a/neurom/utils.py b/neurom/utils.py index 5ccf9a87..e8786f31 100644 --- a/neurom/utils.py +++ b/neurom/utils.py @@ -35,13 +35,12 @@ import numpy as np from neurom.core.dataformat import COLS +from neurom.exceptions import NeuroMDeprecationWarning def warn_deprecated(msg): """Issue a deprecation warning.""" - warnings.simplefilter('always', DeprecationWarning) - warnings.warn(msg, category=DeprecationWarning, stacklevel=2) - warnings.simplefilter('default', DeprecationWarning) + warnings.warn(msg, category=NeuroMDeprecationWarning, stacklevel=3) def deprecated(fun_name=None, msg=""): diff --git a/tests/test_utils.py b/tests/test_utils.py index f01a1721..a8dd018d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -29,11 +29,20 @@ """Test neurom.utils.""" import json import warnings +from copy import deepcopy import numpy as np from neurom import utils as nu import pytest +from neurom.exceptions import NeuroMDeprecationWarning + + +def test_warn_deprecated(): + + with pytest.warns(NeuroMDeprecationWarning, match="foo"): + nu.warn_deprecated(msg="foo") + def test_deprecated(): @nu.deprecated(msg='Hello') From a9e3020f242b3ff04cdc244e068afdfc7137dfcb Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 16 Mar 2022 10:55:11 +0100 Subject: [PATCH 12/87] introduce flatten (#1003) --- neurom/apps/morph_stats.py | 4 ++-- neurom/check/morphology_checks.py | 9 +++++---- neurom/core/morphology.py | 16 +++++++++------- neurom/features/neurite.py | 4 ++-- neurom/utils.py | 6 ++++++ neurom/view/plotly_impl.py | 12 ++++++------ tests/test_utils.py | 7 +++++++ 7 files changed, 37 insertions(+), 21 deletions(-) diff --git a/neurom/apps/morph_stats.py b/neurom/apps/morph_stats.py index 9fce3342..9cdd7de0 100644 --- a/neurom/apps/morph_stats.py +++ b/neurom/apps/morph_stats.py @@ -52,7 +52,7 @@ 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, warn_deprecated +from neurom.utils import flatten, NeuromJSON, warn_deprecated L = logging.getLogger(__name__) @@ -107,7 +107,7 @@ def extract_dataframe(morphs, config, n_workers=1): columns = [('property', 'name')] + [ (key1, key2) for key1, data in stats[0][1].items() for key2 in data ] - rows = [[name] + list(chain.from_iterable(features.values() for features in data.values())) + rows = [[name] + list(flatten(features.values() for features in data.values())) for name, data in stats] return pd.DataFrame(columns=pd.MultiIndex.from_tuples(columns), data=rows) diff --git a/neurom/check/morphology_checks.py b/neurom/check/morphology_checks.py index b1e3330c..db1a2bdd 100644 --- a/neurom/check/morphology_checks.py +++ b/neurom/check/morphology_checks.py @@ -30,7 +30,7 @@ Contains functions for checking validity of morphology neurites and somata. """ -from itertools import chain, islice +from itertools import islice import numpy as np from neurom import NeuriteType @@ -40,6 +40,7 @@ from neurom.core.dataformat import COLS from neurom.exceptions import NeuroMError from neurom.morphmath import section_length, segment_length +from neurom.utils import flatten def _read_neurite_type(neurite): @@ -281,9 +282,9 @@ def has_no_dangling_branch(morph): 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))) + dendritic_points = np.array(list(flatten(n.points + for n in iter_neurites(morph) + if n.type != NeuriteType.axon))) def is_dangling(neurite): """Is the neurite dangling?""" diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 0601c418..1a8404a8 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -29,7 +29,6 @@ """Morphology classes and functions.""" from collections import deque -from itertools import chain import warnings import morphio @@ -39,7 +38,7 @@ 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 +from neurom.utils import flatten, warn_deprecated class Section: @@ -283,9 +282,10 @@ def iter_sections(neurites, >>> 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)) + return flatten( + 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): @@ -308,8 +308,10 @@ def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder) neurite_filter=neurite_filter, neurite_order=neurite_order)) - return chain.from_iterable(zip(sec.points[:-1], sec.points[1:]) - for sec in sections) + return flatten( + zip(section.points[:-1], section.points[1:]) + for section in sections + ) def graft_morphology(section): diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 0ce9db3e..d9d8ef0f 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -45,7 +45,6 @@ import logging from functools import partial -from itertools import chain import numpy as np from neurom import morphmath @@ -54,6 +53,7 @@ from neurom.features import NameSpace, feature, bifurcation as bf, section as sf from neurom.geom import convex_hull from neurom.morphmath import interval_lengths +from neurom.utils import flatten feature = partial(feature, namespace=NameSpace.NEURITE) @@ -262,7 +262,7 @@ def _sec_taper_rate(sec): @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))) + return list(flatten(_map_sections(sf.section_meander_angles, neurite))) @feature(shape=(..., 3)) diff --git a/neurom/utils.py b/neurom/utils.py index e8786f31..90ab2a4b 100644 --- a/neurom/utils.py +++ b/neurom/utils.py @@ -31,6 +31,7 @@ import warnings from enum import Enum from functools import wraps +from itertools import chain import numpy as np @@ -130,3 +131,8 @@ def str_to_plane(plane): else: # pragma: no cover coords = COLS.XYZ return coords + + +def flatten(list_of_lists): + """Flatten one level of nesting.""" + return chain.from_iterable(list_of_lists) diff --git a/neurom/view/plotly_impl.py b/neurom/view/plotly_impl.py index d498c1b5..57e8f47a 100644 --- a/neurom/view/plotly_impl.py +++ b/neurom/view/plotly_impl.py @@ -17,7 +17,7 @@ # # 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 +# 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; @@ -28,10 +28,9 @@ """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 @@ -43,6 +42,7 @@ from neurom import COLS, iter_segments, iter_neurites from neurom.core.morphology import Morphology from neurom.view.matplotlib_impl import TREE_COLOR +from neurom.utils import flatten def plot_morph(morph, plane='xy', inline=False, **kwargs): @@ -75,9 +75,9 @@ def _make_trace(morph, plane): 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))) + coords = dict(x=list(flatten((p1[0], p2[0], None) for p1, p2 in segs)), + y=list(flatten((p1[1], p2[1], None) for p1, p2 in segs)), + z=list(flatten((p1[2], p2[2], None) for p1, p2 in segs))) color = TREE_COLOR.get(neurite.root_node.type, 'black') if plane.lower() == '3d': diff --git a/tests/test_utils.py b/tests/test_utils.py index a8dd018d..2cdbd3cb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -106,3 +106,10 @@ class Grade(nu.OrderedEnum): Grade.__gt__(Grade.A, 1) with pytest.raises(NotImplementedError): Grade.__lt__(Grade.A, 1) + + +def test_flatten(): + + a = [[1, 2], [3, 4, 5], [6], [7, 8, 9, 10]] + + assert list(nu.flatten(a)) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] From 1b640912b0838bc1ad6518e2f81ba30d71d6b506 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 18 Mar 2022 11:00:59 +0100 Subject: [PATCH 13/87] Refactor features (#1005) - Refactor features to utilize the iterators and mappers as much as possible. - Reduce the number of nested functions/closures by moving them to section features. - neurom.features.segment_taper_rates were fixed to return signed taper rates. --- CHANGELOG.rst | 1 + neurom/features/morphology.py | 67 ++++++++++++---------- neurom/features/neurite.py | 101 ++++++++++----------------------- neurom/features/section.py | 48 ++++++++++++++++ tests/features/test_section.py | 69 +++++++++++++++++++++- 5 files changed, 182 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 21e19fe7..540b7e24 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Version 3.2.0 ------------- +- Fix ``neurom.features.neurite.segment_taper_rates`` to return signed taper rates. - Fix warning system so that it doesn't change the pre-existing warnings configuration - Fix ``neurom.features.bifurcation.partition_asymmetry`` Uylings variant to not throw for bifurcations with leaves. diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 3159aa69..7ae7eac7 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -63,6 +63,12 @@ feature = partial(feature, namespace=NameSpace.NEURON) +def _map_neurites(function, morph, neurite_type): + return list( + iter_neurites(morph, mapfun=function, filt=is_type(neurite_type)) + ) + + @feature(shape=()) def soma_volume(morph): """Get the volume of a morphology's soma.""" @@ -76,7 +82,7 @@ def soma_surface_area(morph): Note: The surface area is calculated by assuming the soma is spherical. """ - return 4 * math.pi * morph.soma.radius ** 2 + return 4.0 * math.pi * morph.soma.radius ** 2 @feature(shape=()) @@ -88,37 +94,33 @@ def soma_radius(morph): @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. + term_radial_distances = _map_neurites(nf.max_radial_distance, morph, neurite_type) + + return max(term_radial_distances) if term_radial_distances else 0.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))] + return _map_neurites(nf.number_of_sections, morph, 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))] + return _map_neurites(nf.total_length, morph, 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))] + return _map_neurites(nf.total_area, morph, 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))] + return _map_neurites(nf.total_volume, morph, neurite_type) @feature(shape=(...,)) @@ -130,13 +132,13 @@ def trunk_origin_azimuths(morph, neurite_type=NeuriteType.all): 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 morphmath.azimuth_from_vector(vector) + def azimuth(neurite): + """Azimuth of a neurite trunk.""" + return morphmath.azimuth_from_vector( + morphmath.vector(neurite.root_node.points[0], morph.soma.center) + ) - return [_azimuth(n.root_node.points, morph.soma) - for n in iter_neurites(morph, filt=is_type(neurite_type))] + return _map_neurites(azimuth, morph, neurite_type) @feature(shape=(...,)) @@ -149,20 +151,22 @@ def trunk_origin_elevations(morph, neurite_type=NeuriteType.all): The range of the elevation angle [-pi/2, pi/2] radians """ - def _elevation(section, soma): + def elevation(neurite): """Elevation of a section.""" - vector = morphmath.vector(section[0], soma.center) - return morphmath.elevation_from_vector(vector) + return morphmath.elevation_from_vector( + morphmath.vector(neurite.root_node.points[0], morph.soma.center) + ) - return [_elevation(n.root_node.points, morph.soma) - for n in iter_neurites(morph, filt=is_type(neurite_type))] + return _map_neurites(elevation, morph, 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))] + def vector_to_root_node(neurite): + return morphmath.vector(neurite.root_node.points[0], morph.soma.center) + + return _map_neurites(vector_to_root_node, morph, neurite_type) @feature(shape=(...,)) @@ -428,27 +432,28 @@ def _mean_radius(neurite): return points[~valid_max, COLS.R][0] return points[valid_pts, COLS.R].mean() - return [_mean_radius(n) for n in iter_neurites(morph, filt=is_type(neurite_type))] + return _map_neurites(_mean_radius, morph, 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))] + def trunk_section_length(neurite): + return morphmath.section_length(neurite.root_node.points) + + return _map_neurites(trunk_section_length, morph, 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))) + return len(_map_neurites(lambda n: n, morph, 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))] + return _map_neurites(nf.volume_density, morph, neurite_type) @feature(shape=(...,)) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index d9d8ef0f..045d30e7 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -52,8 +52,6 @@ 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 -from neurom.utils import flatten feature = partial(feature, namespace=NameSpace.NEURITE) @@ -62,7 +60,7 @@ 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)))) + return list(map(fun, iterator_type(neurite.root_node))) @feature(shape=()) @@ -75,13 +73,13 @@ def max_radial_distance(neurite): @feature(shape=()) def number_of_segments(neurite): """Number of segments.""" - return sum(len(s.points) - 1 for s in Section.ipreorder(neurite.root_node)) + return sum(_map_sections(sf.number_of_segments, neurite)) @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)) + return len(_map_sections(lambda s: s, neurite, iterator_type=iterator_type)) @feature(shape=()) @@ -105,7 +103,7 @@ def number_of_leaves(neurite): @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()) + return sum(_map_sections(sf.section_length, neurite)) @feature(shape=()) @@ -114,36 +112,31 @@ def total_area(neurite): The area is defined as the sum of the area of the sections. """ - return neurite.area + return sum(_map_sections(sf.section_area, neurite)) @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) + return sum(_map_sections(sf.section_volume, neurite)) @feature(shape=(...,)) def section_lengths(neurite): """Section lengths.""" - return _map_sections(_section_length, neurite) + return _map_sections(sf.section_length, neurite) @feature(shape=(...,)) def section_term_lengths(neurite): """Termination section lengths.""" - return _map_sections(_section_length, neurite, Section.ileaf) + return _map_sections(sf.section_length, neurite, Section.ileaf) @feature(shape=(...,)) def section_bif_lengths(neurite): """Bifurcation section lengths.""" - return _map_sections(_section_length, neurite, Section.ibifurcation_point) + return _map_sections(sf.section_length, neurite, Section.ibifurcation_point) @feature(shape=(...,)) @@ -185,8 +178,11 @@ def _map_segments(func, neurite): `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 + return [ + segment_value + for section in Section.ipreorder(neurite.root_node) + for segment_value in func(section) + ] @feature(shape=(...,)) @@ -198,32 +194,19 @@ def 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:])] + return _map_segments(sf.segment_areas, neurite) @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) + return _map_segments(sf.segment_volumes, 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) + return _map_segments(sf.segment_mean_radii, neurite) @feature(shape=(...,)) @@ -232,15 +215,7 @@ def segment_taper_rates(neurite): 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) + return _map_segments(sf.segment_taper_rates, neurite) @feature(shape=(...,)) @@ -250,31 +225,19 @@ def section_taper_rates(neurite): 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) + return _map_sections(sf.taper_rate, neurite) @feature(shape=(...,)) def segment_meander_angles(neurite): """Inter-segment opening angles in a section.""" - return list(flatten(_map_sections(sf.section_meander_angles, neurite))) + return _map_segments(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) + return _map_segments(sf.segment_midpoints, neurite) @feature(shape=(...,)) @@ -282,31 +245,28 @@ def segment_path_lengths(neurite): """Returns pathlengths between all non-root points and their root point.""" pathlength = {} - def _get_pathlength(section): + def segments_pathlength(section): if section.id not in pathlength: if section.parent: - pathlength[section.id] = section.parent.length + _get_pathlength(section.parent) + pathlength[section.id] = section.parent.length + pathlength[section.parent.id] else: pathlength[section.id] = 0 - return pathlength[section.id] + return pathlength[section.id] + np.cumsum(sf.segment_lengths(section)) - 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 [] + return _map_segments(segments_pathlength, neurite) @feature(shape=(...,)) def segment_radial_distances(neurite, origin=None): """Returns the list of distances between all segment mid points and origin.""" + pos = neurite.root_node.points[0] if origin is None else origin - def _radial_distances(sec, pos): + def radial_distances(section): """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]) + mid_pts = 0.5 * (section.points[:-1, COLS.XYZ] + section.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)] + return _map_segments(radial_distances, neurite) @feature(shape=(...,)) @@ -487,8 +447,7 @@ def section_end_distances(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]] + return [morphmath.principal_direction_extent(neurite.points[:, COLS.XYZ])[direction]] @feature(shape=(...,)) diff --git a/neurom/features/section.py b/neurom/features/section.py index cb8da379..03677804 100644 --- a/neurom/features/section.py +++ b/neurom/features/section.py @@ -32,6 +32,7 @@ from neurom import morphmath as mm from neurom.core.dataformat import COLS +from neurom.core.morphology import iter_segments from neurom.morphmath import interval_lengths @@ -40,6 +41,11 @@ def section_path_length(section): return sum(s.length for s in section.iupstream()) +def section_length(section): + """Length of a section.""" + return section.length + + def section_volume(section): """Volume of a section.""" return section.volume @@ -87,11 +93,53 @@ def branch_order(section): return sum(1 for _ in section.iupstream()) - 1 +def taper_rate(section): + """Taper rate from fit along a section.""" + pts = section.points + path_distances = np.cumsum(interval_lengths(pts, prepend_zero=True)) + return np.polynomial.polynomial.polyfit(path_distances, 2.0 * pts[:, COLS.R], 1)[1] + + +def number_of_segments(section): + """Returns the number of segments within a section.""" + return len(section.points) - 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 segment_areas(section): + """Returns the list of segment areas within the section.""" + return [mm.segment_area(seg) for seg in iter_segments(section)] + + +def segment_volumes(section): + """Returns the list of segment volumes within the section.""" + return [mm.segment_volume(seg) for seg in iter_segments(section)] + + +def segment_mean_radii(section): + """Returns the list of segment mean radii within the section.""" + pts = section.points[:, COLS.R] + return np.divide(np.add(pts[:-1], pts[1:]), 2.0).tolist() + + +def segment_midpoints(section): + """Returns the list of segment midpoints within the section.""" + pts = section.points[:, COLS.XYZ] + return np.divide(np.add(pts[:-1], pts[1:]), 2.0).tolist() + + +def segment_taper_rates(section): + """Returns the list of segment taper rates within the section.""" + pts = section.points[:, COLS.XYZR] + diff = np.diff(pts, axis=0) + distance = np.linalg.norm(diff[:, COLS.XYZ], axis=1) + return np.divide(2.0 * diff[:, COLS.R], distance).tolist() + + def section_radial_distance(section, origin): """Return the radial distances of a tree section to a given origin point. diff --git a/tests/features/test_section.py b/tests/features/test_section.py index 7a35c62b..72982b37 100644 --- a/tests/features/test_section.py +++ b/tests/features/test_section.py @@ -32,14 +32,17 @@ import warnings from io import StringIO from pathlib import Path +from unittest.mock import Mock +import pytest import numpy as np +from numpy import testing as npt +from mock import Mock + from neurom import load_morphology, iter_sections from neurom import morphmath from neurom.features import section -import pytest - DATA_PATH = Path(__file__).parent.parent / 'data' H5_PATH = DATA_PATH / 'h5/v1/' SWC_PATH = DATA_PATH / 'swc/' @@ -47,6 +50,28 @@ SECTION_ID = 0 +def test_section_length(): + sec = Mock(length=3.2) + npt.assert_almost_equal(section.section_length(sec), 3.2) + + +def test_number_of_segments(): + sec = Mock(points=np.array([[0., 1., 2., 1.], [3., 4., 5., 1.], [6., 7., 8., 1.]])) + npt.assert_almost_equal(section.number_of_segments(sec), 2) + + +def test_section_taper_rate(): + # Note: taper rate is calculated on the diameters + sec = Mock(points=np.array([[0., 0., 0., 2.], [1., 0., 0., 1.], [2., 0., 0., 0.]])) + npt.assert_almost_equal(section.taper_rate(sec), -2.) + + +def test_segment_taper_rates(): + # Note: taper rate is calculated on the diameters + sec = Mock(points=np.array([[0., 0., 0., 2.], [1., 0., 0., 1.], [2., 0., 0., 0.]])) + npt.assert_almost_equal(section.segment_taper_rates(sec), [-2., -2.]) + + def test_section_area(): sec = load_morphology(StringIO(u"""((CellBody) (0 0 0 2)) ((Dendrite) @@ -56,6 +81,46 @@ def test_section_area(): assert math.pi * 1 * 2 * 1 == area +def test_segment_areas(): + sec = load_morphology(StringIO(u"""((CellBody) (0 0 0 2)) + ((Dendrite) + (0 0 0 4) + (1 0 0 4) + (2 0 0 4))"""), reader='asc').sections[SECTION_ID] + + npt.assert_allclose(section.segment_areas(sec), [2. * np.pi * 2. * 1.] * 2) + + +def test_segment_volumes(): + sec = load_morphology(StringIO(u"""((CellBody) (0 0 0 2)) + ((Dendrite) + (0 0 0 4) + (1 0 0 4) + (2 0 0 4))"""), reader='asc').sections[SECTION_ID] + + npt.assert_allclose(section.segment_areas(sec), [np.pi * 4. * 1.] * 2) + + +def test_segment_mean_radii(): + sec = load_morphology(StringIO(u"""((CellBody) (0 0 0 2)) + ((Dendrite) + (0 0 0 2) + (1 0 0 4) + (2 0 0 6))"""), reader='asc').sections[SECTION_ID] + + npt.assert_allclose(section.segment_mean_radii(sec), [1.5, 2.5]) + + +def test_segment_midpoints(): + sec = load_morphology(StringIO(u"""((CellBody) (0 0 0 2)) + ((Dendrite) + (0 0 0 2) + (1 0 0 4) + (2 0 0 6))"""), reader='asc').sections[SECTION_ID] + + npt.assert_allclose(section.segment_midpoints(sec), [[0.5, 0., 0.], [1.5, 0., 0.]]) + + def test_section_tortuosity(): sec_a = load_morphology(StringIO(u""" ((CellBody) (0 0 0 2)) From fdf9f78be79508e34c0349936e233c184279c955 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 18 Mar 2022 18:05:11 +0100 Subject: [PATCH 14/87] Generalize partition asymmetry (#1006) Allow passing downstream iterator_type in partition asymmetry functions --- neurom/features/bifurcation.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/neurom/features/bifurcation.py b/neurom/features/bifurcation.py index 6c628d15..423a3ef2 100644 --- a/neurom/features/bifurcation.py +++ b/neurom/features/bifurcation.py @@ -32,6 +32,7 @@ from neurom import morphmath from neurom.exceptions import NeuroMError from neurom.core.dataformat import COLS +from neurom.core.morphology import Section from neurom.features.section import section_mean_radius @@ -84,7 +85,7 @@ def remote_bifurcation_angle(bif_point): bif_point.children[1].points[-1]) -def bifurcation_partition(bif_point): +def bifurcation_partition(bif_point, iterator_type=Section.ipreorder): """Calculate the partition at a bifurcation point. We first ensure that the input point has only two children. @@ -94,12 +95,12 @@ def bifurcation_partition(bif_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())) + n, m = partition_pair(bif_point, iterator_type=iterator_type) + return max(n, m) / min(n, m) -def partition_asymmetry(bif_point, uylings=False): +def partition_asymmetry(bif_point, uylings=False, iterator_type=Section.ipreorder): """Calculate the partition asymmetry at a bifurcation point. By default partition asymmetry is defined as in https://www.ncbi.nlm.nih.gov/pubmed/18568015. @@ -113,8 +114,7 @@ def partition_asymmetry(bif_point, uylings=False): """ _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())) + n, m = partition_pair(bif_point, iterator_type=iterator_type) if n == m == 1: # By definition the asymmetry A(1, 1) is zero @@ -125,16 +125,17 @@ def partition_asymmetry(bif_point, uylings=False): return abs(n - m) / abs(n + m - c) -def partition_pair(bif_point): +def partition_pair(bif_point, iterator_type=Section.ipreorder): """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) + return ( + float(len(list(iterator_type(bif_point.children[0])))), + float(len(list(iterator_type(bif_point.children[1])))), + ) def sibling_ratio(bif_point, method='first'): From ba2961189511073261c250e6965b57cdfcfd6d4b Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 21 Mar 2022 11:05:01 +0100 Subject: [PATCH 15/87] Fix principal direction extent (#1008) Fixes principal_direction_extent to calculate correctly the extent of the projections along the principal directions. --- CHANGELOG.rst | 1 + neurom/morphmath.py | 28 ++++++-------- tests/features/test_get_features.py | 16 ++++---- tests/test_morphmath.py | 57 +++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 540b7e24..80313f93 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Version 3.2.0 ------------- +- Fix ``neurom.morphmath.principal_direction_extent`` to calculate correctly the pca extent. - Fix ``neurom.features.neurite.segment_taper_rates`` to return signed taper rates. - Fix warning system so that it doesn't change the pre-existing warnings configuration - Fix ``neurom.features.bifurcation.partition_asymmetry`` Uylings variant to not throw diff --git a/neurom/morphmath.py b/neurom/morphmath.py index b01c5163..41e090dd 100644 --- a/neurom/morphmath.py +++ b/neurom/morphmath.py @@ -462,17 +462,17 @@ def sphere_area(r): def principal_direction_extent(points): """Calculate the extent of a set of 3D points. - The extent is defined as the maximum distance between - the projections on the principal directions of the covariance matrix - of the points. + The extent is defined as the maximum distance between the projections on the principal + directions of the covariance matrix of the points. - Parameter: - points : a 2D numpy array of points + Args: + points : a 2D numpy array of points with 2 or 3 columns for (x, y, z) Returns: extents : the extents for each of the eigenvectors of the cov matrix - eigs : eigenvalues of the covariance matrix - eigv : respective eigenvectors of the covariance matrix + + Note: + Direction extents are not ordered from largest to smallest. """ # pca can be biased by duplicate points points = np.unique(points, axis=0) @@ -483,14 +483,8 @@ def principal_direction_extent(points): # principal components _, eigv = pca(points) - extent = np.zeros(3) - - for i in range(eigv.shape[1]): - # orthogonal projection onto the direction of the v component - scalar_projs = np.sort(np.array([np.dot(p, eigv[:, i]) for p in points])) - extent[i] = scalar_projs[-1] - - if scalar_projs[0] < 0.: - extent -= scalar_projs[0] + # for each eigenvector calculate the scalar projection of the points on it (n_points, n_eigv) + scalar_projections = points.dot(eigv) - return extent + # and return the range of the projections (abs(max - min)) along each column (eigenvector) + return np.ptp(scalar_projections, axis=0) diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index 6661b89e..a78efa7c 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -793,18 +793,18 @@ def test_section_term_radial_distances(): 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]) + assert_allclose(principal_dir, [10.99514 , 10.997688]) # test with a realistic morphology m = nm.load_morphology(DATA_PATH / 'h5/v1' / 'bio_neuron-000.h5') p_ref = [ - 1672.969491, - 142.437047, - 224.607978, - 415.50613, - 429.830081, - 165.954097, - 346.832825, + 1210.569727, + 38.493958, + 147.098687, + 288.226628, + 330.166506, + 152.396521, + 293.913857 ] p = features.get('principal_direction_extents', m) assert_allclose(p, p_ref, rtol=1e-6) diff --git a/tests/test_morphmath.py b/tests/test_morphmath.py index 0188ccfa..6119f7b6 100644 --- a/tests/test_morphmath.py +++ b/tests/test_morphmath.py @@ -29,6 +29,7 @@ from math import fabs, pi, sqrt import numpy as np +from numpy import testing as npt from neurom import morphmath as mm from neurom.core.dataformat import Point from numpy.random import uniform @@ -532,3 +533,59 @@ def test_spherical_coordinates(): new_elevation, new_azimuth = mm.spherical_from_vector(vect) assert np.allclose([elevation, azimuth], [new_elevation, new_azimuth]) + + +def test_principal_direction_extent(): + + # test with points on a circle with radius 0.5, and center at 0.0 + circle_points = np.array([ + [ 5.0e-01, 0.0e+00, 0.0e+00], + [ 4.7e-01, 1.6e-01, 0.0e+00], + [ 3.9e-01, 3.1e-01, 0.0e+00], + [ 2.7e-01, 4.2e-01, 0.0e+00], + [ 1.2e-01, 4.8e-01, 0.0e+00], + [-4.1e-02, 5.0e-01, 0.0e+00], + [-2.0e-01, 4.6e-01, 0.0e+00], + [-3.4e-01, 3.7e-01, 0.0e+00], + [-4.4e-01, 2.4e-01, 0.0e+00], + [-5.0e-01, 8.2e-02, 0.0e+00], + [-5.0e-01, -8.2e-02, 0.0e+00], + [-4.4e-01, -2.4e-01, 0.0e+00], + [-3.4e-01, -3.7e-01, 0.0e+00], + [-2.0e-01, -4.6e-01, 0.0e+00], + [-4.1e-02, -5.0e-01, 0.0e+00], + [ 1.2e-01, -4.8e-01, 0.0e+00], + [ 2.7e-01, -4.2e-01, 0.0e+00], + [ 3.9e-01, -3.1e-01, 0.0e+00], + [ 4.7e-01, -1.6e-01, 0.0e+00], + [ 5.0e-01, -1.2e-16, 0.0e+00] + ]) + + npt.assert_allclose( + mm.principal_direction_extent(circle_points), + [1., 1., 0.], atol=1e-6, + ) + + # extent should be invariant to translations + npt.assert_allclose( + mm.principal_direction_extent(circle_points + 100.), + [1., 1., 0.], atol=1e-6, + ) + npt.assert_allclose( + mm.principal_direction_extent(circle_points - 100.), + [1., 1., 0.], atol=1e-6, + ) + + cross_3D_points = np.array([ + [-5.2, 0.0, 0.0], + [ 4.8, 0.0, 0.0], + [ 0.0,-1.3, 0.0], + [ 0.0, 4.7, 0.0], + [ 0.0, 0.0,-11.2], + [ 0.0, 0.0, 0.8], + ]) + + npt.assert_allclose( + sorted(mm.principal_direction_extent(cross_3D_points)), + [6.0, 10.0, 12.0], atol=0.1, + ) From cef7800dddb241318045856767312771f104dc8a Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 28 Mar 2022 15:07:46 +0200 Subject: [PATCH 16/87] Shape related features (#1013) Added aspect_ratio, circularity, and shape_factor features, calculated on projected planes. --- CHANGELOG.rst | 1 + neurom/features/morphology.py | 90 ++++++++++++- neurom/features/neurite.py | 2 +- neurom/features/section.py | 5 + neurom/geom/__init__.py | 25 +--- neurom/morphmath.py | 59 +++++++++ setup.py | 7 +- tests/features/test_get_features.py | 79 +++++++++++ tests/features/test_morphology.py | 60 +++++++++ tests/features/test_neurite.py | 2 +- tests/features/test_section.py | 3 + tests/geom/test_geom.py | 9 +- tests/test_morphmath.py | 199 ++++++++++++++++++++++++++++ 13 files changed, 504 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 80313f93..1a4feecd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Version 3.2.0 ------------- +- Add features ``neurom.features.morphology.(aspect_ration, circularity, shape_factor)``` - Fix ``neurom.morphmath.principal_direction_extent`` to calculate correctly the pca extent. - Fix ``neurom.features.neurite.segment_taper_rates`` to return signed taper rates. - Fix warning system so that it doesn't change the pre-existing warnings configuration diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 7ae7eac7..5abac73f 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -55,9 +55,9 @@ from neurom.core.dataformat import COLS from neurom.core.types import NeuriteType from neurom.exceptions import NeuroMError -from neurom.features import feature, NameSpace, neurite as nf +from neurom.features import feature, NameSpace, neurite as nf, section as sf from neurom.utils import str_to_plane -from neurom.geom import convex_hull +from neurom.morphmath import convex_hull feature = partial(feature, namespace=NameSpace.NEURON) @@ -589,14 +589,10 @@ def volume_density(morph, neurite_type=NeuriteType.all): .. note:: Returns `np.nan` if the convex hull computation fails or there are not points available due to neurite type filtering. """ - - def get_points(neurite): - return neurite.points[:, COLS.XYZ] - # note: duplicate points are present but do not affect convex hull calculation points = [ point - for point_list in iter_neurites(morph, mapfun=get_points, filt=is_type(neurite_type)) + for point_list in iter_neurites(morph, mapfun=sf.section_points, filt=is_type(neurite_type)) for point in point_list ] @@ -608,3 +604,83 @@ def get_points(neurite): total_volume = sum(iter_neurites(morph, mapfun=nf.total_volume, filt=is_type(neurite_type))) return total_volume / morph_hull.volume + + +def _unique_projected_points(morph, projection_plane, neurite_type): + + key = "".join(sorted(projection_plane.lower())) + + try: + axes = {"xy": COLS.XY, "xz": COLS.XZ, "yz": COLS.YZ}[key] + + except KeyError as e: + + raise NeuroMError( + f"Invalid 'projection_plane' argument {projection_plane}. " + f"Please select 'xy', 'xz', or 'yz'." + ) from e + + points = list( + iter_neurites(morph, mapfun=sf.section_points, filt=is_type(neurite_type)) + ) + + if len(points) == 0: + return np.empty(shape=(0, 3), dtype=np.float32) + + return np.unique(np.vstack(points), axis=0)[:, axes] + + +@feature(shape=()) +def aspect_ratio(morph, neurite_type=NeuriteType.all, projection_plane="xy"): + """Calculates the min/max ratio of the principal direction extents along the plane. + + Args: + morph: Morphology object. + neurite_type: The neurite type to use. By default all neurite types are used. + projection_plane: Projection plane to use for the calculation. One of ('xy', 'xz', 'yz'). + + Returns: + The aspect ratio feature of the morphology points. + """ + projected_points = _unique_projected_points(morph, projection_plane, neurite_type) + return [] if len(projected_points) == 0 else morphmath.aspect_ratio(projected_points) + + +@feature(shape=()) +def circularity(morph, neurite_type=NeuriteType.all, projection_plane="xy"): + """Calculates the circularity of the morphology points along the plane. + + The circularity is defined as the 4 * pi * area of the convex hull over its + perimeter. + + Args: + morph: Morphology object. + neurite_type: The neurite type to use. By default all neurite types are used. + projection_plane: Projection plane to use for the calculation. One of + ('xy', 'xz', 'yz'). + + Returns: + The circularity of the morphology points. + """ + projected_points = _unique_projected_points(morph, projection_plane, neurite_type) + return [] if len(projected_points) == 0 else morphmath.circularity(projected_points) + + +@feature(shape=()) +def shape_factor(morph, neurite_type=NeuriteType.all, projection_plane="xy"): + """Calculates the shape factor of the morphology points along the plane. + + The shape factor is defined as the ratio of the convex hull area over max squared + pairwise distance of the morphology points. + + Args: + morph: Morphology object. + neurite_type: The neurite type to use. By default all neurite types are used. + projection_plane: Projection plane to use for the calculation. One of + ('xy', 'xz', 'yz'). + + Returns: + The shape factor of the morphology points. + """ + projected_points = _unique_projected_points(morph, projection_plane, neurite_type) + return [] if len(projected_points) == 0 else morphmath.shape_factor(projected_points) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 045d30e7..0439d889 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -51,7 +51,7 @@ 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 convex_hull feature = partial(feature, namespace=NameSpace.NEURITE) diff --git a/neurom/features/section.py b/neurom/features/section.py index 03677804..259699f3 100644 --- a/neurom/features/section.py +++ b/neurom/features/section.py @@ -36,6 +36,11 @@ from neurom.morphmath import interval_lengths +def section_points(section): + """Returns the points in the section.""" + return section.points[:, COLS.XYZ] + + def section_path_length(section): """Path length from section to root.""" return sum(s.length for s in section.iupstream()) diff --git a/neurom/geom/__init__.py b/neurom/geom/__init__.py index f23f865c..9eafc5d8 100644 --- a/neurom/geom/__init__.py +++ b/neurom/geom/__init__.py @@ -31,12 +31,11 @@ import logging import numpy as np -from scipy.spatial import ConvexHull -from scipy.spatial.qhull import QhullError + +import neurom.morphmath from neurom.core.dataformat import COLS from neurom.geom.transform import translate, rotate - L = logging.getLogger(__name__) @@ -50,24 +49,10 @@ def bounding_box(obj): np.max(obj.points[:, COLS.XYZ], axis=0)]) -def convex_hull(point_data): - """Get the convex hull from point data. +def convex_hull(obj): + """Get the convex hull from object containing points. Returns: scipy.spatial.ConvexHull object if successful, otherwise None """ - if len(point_data) == 0: - L.exception( - "Failure to compute convex hull because there are no points" - ) - return None - - points = np.asarray(point_data)[:, COLS.XYZ] - - try: - return ConvexHull(points) - except QhullError: - L.exception( - "Failure to compute convex hull because points like on a 2D plane." - ) - return None + return neurom.morphmath.convex_hull(obj.points[:, COLS.XYZ]) diff --git a/neurom/morphmath.py b/neurom/morphmath.py index 41e090dd..248c2e9e 100644 --- a/neurom/morphmath.py +++ b/neurom/morphmath.py @@ -28,13 +28,20 @@ """Mathematical and geometrical functions used to compute morphometrics.""" import math +import logging from itertools import combinations import numpy as np +from scipy.spatial import ConvexHull +from scipy.spatial.qhull import QhullError +from scipy.spatial.distance import cdist from neurom.core.dataformat import COLS +L = logging.getLogger(__name__) + + def vector(p1, p2): """Compute vector between two 3D points. @@ -488,3 +495,55 @@ def principal_direction_extent(points): # and return the range of the projections (abs(max - min)) along each column (eigenvector) return np.ptp(scalar_projections, axis=0) + + +def convex_hull(points): + """Get the convex hull from an array of points. + + Returns: + scipy.spatial.ConvexHull object if successful, otherwise None + """ + if len(points) == 0: + L.exception( + "Failure to compute convex hull because there are no points" + ) + return None + + try: + return ConvexHull(points) + except QhullError: + L.exception( + "Failure to compute convex hull because of geometrical degeneracy." + ) + return None + + +def aspect_ratio(points): + """Computes the min/max ratio of the principal direction extents.""" + extents = principal_direction_extent(points) + return float(extents.min() / extents.max()) + + +def circularity(points): + """Computes circularity as 4 * pi * area / perimeter^2. + + Note: For 2D points, ConvexHull.volume corresponds to its area and ConvexHull.area + to its perimeter. + """ + hull = convex_hull(points) + return 4.0 * np.pi * hull.volume / hull.area**2 + + +def shape_factor(points): + """Computes area over max pairwise distance squared. + + Defined in doi: 10.1109/ICoAC44903.2018.8939083 + + Note: For 2D points, ConvexHull.volume corresponds to its area. + """ + hull = convex_hull(points) + hull_points = points[hull.vertices] + + max_pairwise_distance = np.max(cdist(hull_points, hull_points)) + + return hull.volume / max_pairwise_distance**2 diff --git a/setup.py b/setup.py index 6c0cbf07..8adcd2bc 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,12 @@ name='neurom', extras_require={ 'plotly': ['plotly>=3.6.0', 'psutil>=5.5.1'], # for plotly image saving - 'docs': ['sphinx', 'sphinx-bluebrain-theme', 'sphinx-autorun'], + 'docs': [ + 'Jinja2<3.1', # New release 3.1.0 breaks sphinx-bluebrain-theme + 'sphinx', + 'sphinx-bluebrain-theme', + 'sphinx-autorun', + ], }, include_package_data=True, python_requires='>=3.7', diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index a78efa7c..6b480c8c 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -41,6 +41,7 @@ from neurom.features import neurite, NameSpace import pytest +from numpy import testing as npt from numpy.testing import assert_allclose DATA_PATH = Path(__file__).parent.parent / 'data' @@ -861,3 +862,81 @@ def test_total_depth(): features.get('total_depth', NRN, neurite_type=nm.BASAL_DENDRITE), 51.64143 ) + + +def test_aspect_ratio(): + + morph = load_morphology(DATA_PATH / "neurolucida/bio_neuron-000.asc") + + npt.assert_almost_equal( + features.get("aspect_ratio", morph, neurite_type=nm.AXON, projection_plane="xy"), + 0.710877, + decimal=6 + ) + npt.assert_almost_equal( + features.get("aspect_ratio", morph, neurite_type=nm.AXON, projection_plane="xz"), + 0.222268, + decimal=6 + ) + npt.assert_almost_equal( + features.get("aspect_ratio", morph, neurite_type=nm.AXON, projection_plane="yz"), + 0.315263, + decimal=6 + ) + npt.assert_almost_equal( + features.get("aspect_ratio", morph), + 0.731076, + decimal=6 + ) + + +def test_circularity(): + + morph = load_morphology(DATA_PATH / "neurolucida/bio_neuron-000.asc") + + npt.assert_almost_equal( + features.get("circularity", morph, neurite_type=nm.AXON, projection_plane="xy"), + 0.722613, + decimal=6 + ) + npt.assert_almost_equal( + features.get("circularity", morph, neurite_type=nm.AXON, projection_plane="xz"), + 0.378692, + decimal=6 + ) + npt.assert_almost_equal( + features.get("circularity", morph, neurite_type=nm.AXON, projection_plane="yz"), + 0.527657, + decimal=6 + ) + npt.assert_almost_equal( + features.get("circularity", morph), + 0.730983, + decimal=6 + ) + + +def test_shape_factor(): + + morph = load_morphology(DATA_PATH / "neurolucida/bio_neuron-000.asc") + + npt.assert_almost_equal( + features.get("shape_factor", morph, neurite_type=nm.AXON, projection_plane="xy"), + 0.356192, + decimal=6 + ) + npt.assert_almost_equal( + features.get("shape_factor", morph, neurite_type=nm.AXON, projection_plane="xz"), + 0.131547, + decimal=6 + ) + npt.assert_almost_equal( + features.get("shape_factor", morph, neurite_type=nm.AXON, projection_plane="yz"), + 0.194558, + decimal=6 + ) + npt.assert_almost_equal( + features.get("shape_factor", morph), + 0.364678, + decimal=6 + ) diff --git a/tests/features/test_morphology.py b/tests/features/test_morphology.py index 59b4cf11..2337c839 100644 --- a/tests/features/test_morphology.py +++ b/tests/features/test_morphology.py @@ -36,6 +36,7 @@ import numpy as np import pytest from morphio import PointLevel, SectionType +from numpy import testing as npt from numpy.testing import assert_allclose from numpy.testing import assert_almost_equal from numpy.testing import assert_array_almost_equal @@ -665,3 +666,62 @@ def test_volume_density(): assert np.isnan( morphology.volume_density(morph, neurite_type=NeuriteType.apical_dendrite), ) + + +def test_unique_projected_points(): + + morph = load_swc(""" + 1 1 0.5 0.5 0.5 0.5 -1 + 2 3 0.2 0.2 0.7 0.1 1 + 3 3 0.0 0.0 1.0 0.1 2 + 4 3 0.2 0.7 0.7 0.1 1 + 5 3 0.0 1.0 1.0 0.1 4 + 6 3 0.7 0.2 0.7 0.1 1 + 7 3 1.0 0.0 1.0 0.1 6 + 8 3 0.2 0.2 0.2 0.1 1 + 9 3 0.0 0.0 0.0 0.1 8 + 10 3 0.2 0.7 0.2 0.1 1 + 11 3 0.0 1.0 0.0 0.1 10 + 12 5 0.7 0.7 0.2 0.1 1 + 13 5 1.0 1.0 0.0 0.1 12 + 14 2 0.7 0.2 0.2 0.1 1 + 15 2 1.0 0.0 0.0 0.1 14 + 16 3 0.7 0.7 0.7 0.1 1 + 17 3 1.0 1.0 1.0 0.1 16 + """) + + for plane, enalp in zip(("xy", "xz", "yz"), ("yx", "zx", "zy")): + npt.assert_allclose( + morphology._unique_projected_points(morph, plane, NeuriteType.all), + morphology._unique_projected_points(morph, enalp, NeuriteType.all), + ) + + npt.assert_allclose( + morphology._unique_projected_points(morph, "xy", NeuriteType.all), + [ + [0. , 0. ], [0. , 0. ], [0. , 1. ], [0. , 1. ], [0.2, 0.2], [0.2, 0.2], + [0.2, 0.7], [0.2, 0.7], [0.7, 0.2], [0.7, 0.2], [0.7, 0.7], [0.7, 0.7], + [1. , 0. ], [1. , 0. ], [1. , 1. ], [1. , 1. ], + ] + ) + npt.assert_allclose( + morphology._unique_projected_points(morph, "xz", NeuriteType.all), + [ + [0. , 0. ], [0. , 1. ], [0. , 0. ], [0. , 1. ], [0.2, 0.2], [0.2, 0.7], + [0.2, 0.2], [0.2, 0.7], [0.7, 0.2], [0.7, 0.7], [0.7, 0.2], [0.7, 0.7], + [1. , 0. ], [1. , 1. ], [1. , 0. ], [1. , 1. ], + ] + ) + npt.assert_allclose( + morphology._unique_projected_points(morph, "yz", NeuriteType.all), + [ + [0. , 0. ], [0. , 1. ], [1. , 0. ], [1. , 1. ], [0.2, 0.2], [0.2, 0.7], + [0.7, 0.2], [0.7, 0.7], [0.2, 0.2], [0.2, 0.7], [0.7, 0.2], [0.7, 0.7], + [0. , 0. ], [0. , 1. ], [1. , 0. ], [1. , 1. ], + ] + ) + + with pytest.raises(NeuroMError): + morphology._unique_projected_points(morph, "airplane", NeuriteType.all) + + assert len(morphology._unique_projected_points(morph, "yz", NeuriteType.apical_dendrite)) == 0 diff --git a/tests/features/test_neurite.py b/tests/features/test_neurite.py index 795549ef..90bea2a3 100644 --- a/tests/features/test_neurite.py +++ b/tests/features/test_neurite.py @@ -65,7 +65,7 @@ def test_number_of_leaves(): def test_neurite_volume_density(): vol = np.array(morphology.total_volume_per_neurite(NRN)) - hull_vol = np.array([convex_hull(n.points).volume for n in nm.iter_neurites(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 diff --git a/tests/features/test_section.py b/tests/features/test_section.py index 72982b37..d549410a 100644 --- a/tests/features/test_section.py +++ b/tests/features/test_section.py @@ -49,6 +49,9 @@ NRN = load_morphology(H5_PATH / 'Neuron.h5') SECTION_ID = 0 +def test_section_points(): + sec = Mock(points=np.array([[0., 1., 2., 1.], [3., 4., 5., 1.], [6., 7., 8., 1.]])) + npt.assert_almost_equal(section.section_points(sec), [[0., 1., 2.], [3., 4., 5.], [6., 7., 8.]]) def test_section_length(): sec = Mock(length=3.2) diff --git a/tests/geom/test_geom.py b/tests/geom/test_geom.py index f75365c2..e1ef70bb 100644 --- a/tests/geom/test_geom.py +++ b/tests/geom/test_geom.py @@ -77,7 +77,7 @@ def test_convex_hull_points(): # This leverages scipy ConvexHull and we don't want # to re-test scipy, so simply check that the points are the same. - hull = geom.convex_hull(NRN.points[:, COLS.XYZ]) + hull = geom.convex_hull(NRN) assert np.alltrue(hull.points == NRN.points[:, :3]) @@ -85,10 +85,5 @@ def test_convex_hull_volume(): # This leverages scipy ConvexHull and we don't want # to re-test scipy, so simply regression test the volume - hull = geom.convex_hull(NRN.points[:, COLS.XYZ]) + hull = geom.convex_hull(NRN) assert_almost_equal(hull.volume, 208641, decimal=0) - - -def test_convex_hull_invalid(): - assert geom.convex_hull([]) is None - assert geom.convex_hull([[1., 0., 0.], [1., 0., 0.]]) is None diff --git a/tests/test_morphmath.py b/tests/test_morphmath.py index 6119f7b6..181aaf8c 100644 --- a/tests/test_morphmath.py +++ b/tests/test_morphmath.py @@ -26,6 +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. +from pathlib import Path from math import fabs, pi, sqrt import numpy as np @@ -589,3 +590,201 @@ def test_principal_direction_extent(): sorted(mm.principal_direction_extent(cross_3D_points)), [6.0, 10.0, 12.0], atol=0.1, ) + + +def test_convex_hull_invalid(): + + assert mm.convex_hull([]) is None + assert mm.convex_hull([[1., 0., 0.], [1., 0., 0.]]) is None + + +def _shape_datasets(): + + return { + "cross-3D": np.array([ + [-5.2, 0.0, 0.0], + [ 4.8, 0.0, 0.0], + [ 0.0,-1.3, 0.0], + [ 0.0, 4.7, 0.0], + [ 0.0, 0.0,-11.2], + [ 0.0, 0.0, 0.8], + ]), + "cross-2D": np.array([ + [ 0.0, 0.0], + [ 0.0, 0.0], + [-1.3, 0.0], + [ 4.7, 0.0], + [ 0.0,-11.2], + [ 0.0, 0.8], + ]), + "circle-2D": np.array([ + [ 5.0e-01, 0.0e+00], + [ 4.7e-01, 1.6e-01], + [ 3.9e-01, 3.1e-01], + [ 2.7e-01, 4.2e-01], + [ 1.2e-01, 4.8e-01], + [-4.1e-02, 5.0e-01], + [-2.0e-01, 4.6e-01], + [-3.4e-01, 3.7e-01], + [-4.4e-01, 2.4e-01], + [-5.0e-01, 8.2e-02], + [-5.0e-01, -8.2e-02], + [-4.4e-01, -2.4e-01], + [-3.4e-01, -3.7e-01], + [-2.0e-01, -4.6e-01], + [-4.1e-02, -5.0e-01], + [ 1.2e-01, -4.8e-01], + [ 2.7e-01, -4.2e-01], + [ 3.9e-01, -3.1e-01], + [ 4.7e-01, -1.6e-01], + [ 5.0e-01, -1.2e-16], + ]), + "square-2D": np.array([ + [ 0.0, 0.0 ], + [ 5.0, 0.0 ], + [10.0, 0.0 ], + [ 0.0, 5.0 ], + [ 0.0, 10.0], + [ 5.0, 10.0], + [10.0, 10.0], + [10.0, 5.0 ], + ]), + "rectangle-2D": np.array([ + [ 0.0, 0.0 ], + [ 5.0, 0.0 ], + [20.0, 0.0 ], + [ 0.0, 5.0 ], + [ 0.0, 10.0], + [ 5.0, 10.0], + [20.0, 10.0], + [20.0, 5.0 ], + ]), + "oval-2D": np.array([ + [ 5.00e-01, 0.00e+00], + [ 4.70e-01, 4.80e-01], + [ 3.90e-01, 9.30e-01], + [ 2.70e-01, 1.26e+00], + [ 1.20e-01, 1.44e+00], + [-4.10e-02, 1.50e+00], + [-2.00e-01, 1.38e+00], + [-3.40e-01, 1.11e+00], + [-4.40e-01, 7.20e-01], + [-5.00e-01, 2.46e-01], + [-5.00e-01, -2.46e-01], + [-4.40e-01, -7.20e-01], + [-3.40e-01, -1.11e+00], + [-2.00e-01, -1.38e+00], + [-4.10e-02, -1.50e+00], + [ 1.20e-01, -1.44e+00], + [ 2.70e-01, -1.26e+00], + [ 3.90e-01, -9.30e-01], + [ 4.70e-01, -4.80e-01], + [ 5.00e-01, -3.60e-16] + ]), + } + + +def test_aspect_ratio(): + + shapes = _shape_datasets() + + npt.assert_allclose( + mm.aspect_ratio(shapes["cross-3D"]), + 0.5, + atol=1e-5 + ) + npt.assert_allclose( + mm.aspect_ratio(shapes["cross-2D"]), + 0.5, + atol=1e-5 + ) + npt.assert_allclose( + mm.aspect_ratio(shapes["circle-2D"]), + 1.0, + atol=1e-5 + ) + npt.assert_allclose( + mm.aspect_ratio(shapes["square-2D"]), + 1.0, + atol=1e-5 + ) + npt.assert_allclose( + mm.aspect_ratio(shapes["rectangle-2D"]), + 0.5, + atol=1e-5 + ) + npt.assert_allclose( + mm.aspect_ratio(shapes["oval-2D"]), + 0.333333, + atol=1e-5 + ) + + +def test_circularity(): + + shapes = _shape_datasets() + + npt.assert_allclose( + mm.circularity(shapes["cross-3D"]), + 0.051904, + atol=1e-5 + ) + npt.assert_allclose( + mm.circularity(shapes["cross-2D"]), + 0.512329, + atol=1e-5 + ) + npt.assert_allclose( + mm.circularity(shapes["circle-2D"]), + 0.99044, + atol=1e-5 + ) + npt.assert_allclose( + mm.circularity(shapes["square-2D"]), + 0.785398, + atol=1e-5 + ) + npt.assert_allclose( + mm.circularity(shapes["rectangle-2D"]), + 0.698132, + atol=1e-5 + ) + npt.assert_allclose( + mm.circularity(shapes["oval-2D"]), + 0.658071, + atol=1e-5 + ) + +def test_shape_factor(): + shapes = _shape_datasets() + + npt.assert_allclose( + mm.shape_factor(shapes["cross-3D"]), + 0.786988, + atol=1e-5 + ) + npt.assert_allclose( + mm.shape_factor(shapes["cross-2D"]), + 0.244018, + atol=1e-5 + ) + npt.assert_allclose( + mm.shape_factor(shapes["circle-2D"]), + 0.766784, + atol=1e-5 + ) + npt.assert_allclose( + mm.shape_factor(shapes["square-2D"]), + 0.5, + atol=1e-5 + ) + npt.assert_allclose( + mm.shape_factor(shapes["rectangle-2D"]), + 0.4, + atol=1e-5 + ) + npt.assert_allclose( + mm.shape_factor(shapes["oval-2D"]), + 0.257313, + atol=1e-5 + ) From dc54aa8566d59ba08aefb3457412b23297903f49 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 1 Apr 2022 09:12:07 +0200 Subject: [PATCH 17/87] Update `neurom.features.get` docstring with available builtin features (#1016) --- neurom/features/__init__.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index d55355fc..924de8d1 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -190,3 +190,31 @@ def inner(func): # These imports are necessary in order to register the features from neurom.features import neurite, morphology, \ population # noqa, pylint: disable=wrong-import-position + + +def _features_catalogue(): + """Returns a string with all the available builtin features.""" + indentation = "\t" + preamble = "\n .. Builtin Features:\n" + + def format_category(category): + separator = "-" * len(category) + return f"\n{indentation}{category}\n{indentation}{separator}" + + def format_features(features): + prefix = f"\n{indentation}* " + return prefix + f"{prefix}".join(sorted(features)) + + return preamble + "".join( + [ + format_category(category) + format_features(features) + "\n" + for category, features in zip( + ("Population", "Morphology", "Neurite"), + (_POPULATION_FEATURES, _MORPHOLOGY_FEATURES, _NEURITE_FEATURES), + ) + ] + ) + + +# Update the get docstring to include all available builtin features +get.__doc__ += _features_catalogue() From ed518c4e24a5a9829882c5db6f296298c1464b93 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 1 Apr 2022 16:32:02 +0200 Subject: [PATCH 18/87] Sort principal direction extents (#1017) --- CHANGELOG.rst | 2 ++ neurom/features/neurite.py | 7 +++- neurom/morphmath.py | 14 +++++--- tests/features/test_get_features.py | 50 ++++++++++++++++++++++------- tests/test_morphmath.py | 4 +-- 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1a4feecd..8606975d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,8 @@ Changelog Version 3.2.0 ------------- +- ``neurom.features.neurite.principal_direction_extents`` directions correspond to extents + ordered in a descending order. - Add features ``neurom.features.morphology.(aspect_ration, circularity, shape_factor)``` - Fix ``neurom.morphmath.principal_direction_extent`` to calculate correctly the pca extent. - Fix ``neurom.features.neurite.segment_taper_rates`` to return signed taper rates. diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 0439d889..6aac5e9f 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -446,7 +446,12 @@ def section_end_distances(neurite): @feature(shape=(...,)) def principal_direction_extents(neurite, direction=0): - """Principal direction extent of neurites in morphologies.""" + """Principal direction extent of neurites in morphologies. + + Note: + Principal direction extents are always sorted in descending order. Therefore, + by default the maximal principal direction extent is returned. + """ return [morphmath.principal_direction_extent(neurite.points[:, COLS.XYZ])[direction]] diff --git a/neurom/morphmath.py b/neurom/morphmath.py index 248c2e9e..45f07005 100644 --- a/neurom/morphmath.py +++ b/neurom/morphmath.py @@ -479,7 +479,7 @@ def principal_direction_extent(points): extents : the extents for each of the eigenvectors of the cov matrix Note: - Direction extents are not ordered from largest to smallest. + Direction extents are ordered from largest to smallest. """ # pca can be biased by duplicate points points = np.unique(points, axis=0) @@ -488,13 +488,17 @@ def principal_direction_extent(points): points -= np.mean(points, axis=0) # principal components - _, eigv = pca(points) + _, eigenvectors = pca(points) # for each eigenvector calculate the scalar projection of the points on it (n_points, n_eigv) - scalar_projections = points.dot(eigv) + scalar_projections = points.dot(eigenvectors) - # and return the range of the projections (abs(max - min)) along each column (eigenvector) - return np.ptp(scalar_projections, axis=0) + # range of the projections (abs(max - min)) along each column (eigenvector) + extents = np.ptp(scalar_projections, axis=0) + + descending_order = np.argsort(extents)[::-1] + + return extents[descending_order] def convex_hull(points): diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index 6b480c8c..20372b95 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -798,18 +798,46 @@ def test_principal_direction_extents(): # test with a realistic morphology m = nm.load_morphology(DATA_PATH / 'h5/v1' / 'bio_neuron-000.h5') - p_ref = [ - 1210.569727, - 38.493958, - 147.098687, - 288.226628, - 330.166506, - 152.396521, - 293.913857 - ] - p = features.get('principal_direction_extents', m) - assert_allclose(p, p_ref, rtol=1e-6) + assert_allclose( + features.get('principal_direction_extents', m, direction=0), + [ + 1210.569727, + 117.988454, + 147.098687, + 288.226628, + 330.166506, + 152.396521, + 293.913857, + ], + atol=1e-6 + ) + assert_allclose( + features.get('principal_direction_extents', m, direction=1), + [ + 851.730088, + 99.108911, + 116.949436, + 157.171734, + 137.328019, + 20.66982, + 67.157249, + ], + atol=1e-6 + ) + assert_allclose( + features.get('principal_direction_extents', m, direction=2), + [ + 282.961199, + 38.493958, + 40.715183, + 94.061625, + 51.120255, + 10.793167, + 62.808188 + ], + atol=1e-6 + ) def test_total_width(): diff --git a/tests/test_morphmath.py b/tests/test_morphmath.py index 181aaf8c..43bb4dc8 100644 --- a/tests/test_morphmath.py +++ b/tests/test_morphmath.py @@ -587,8 +587,8 @@ def test_principal_direction_extent(): ]) npt.assert_allclose( - sorted(mm.principal_direction_extent(cross_3D_points)), - [6.0, 10.0, 12.0], atol=0.1, + mm.principal_direction_extent(cross_3D_points), + [12.0, 10.0, 6.0], atol=0.1, ) From 623cbd99ede31ae7d6434257593bbc59260c9aa2 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 4 Apr 2022 08:17:10 +0200 Subject: [PATCH 19/87] Return nan if no points in shape features (#1018) --- neurom/features/morphology.py | 6 +++--- tests/features/test_get_features.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 5abac73f..bc7e096a 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -643,7 +643,7 @@ def aspect_ratio(morph, neurite_type=NeuriteType.all, projection_plane="xy"): The aspect ratio feature of the morphology points. """ projected_points = _unique_projected_points(morph, projection_plane, neurite_type) - return [] if len(projected_points) == 0 else morphmath.aspect_ratio(projected_points) + return np.nan if len(projected_points) == 0 else morphmath.aspect_ratio(projected_points) @feature(shape=()) @@ -663,7 +663,7 @@ def circularity(morph, neurite_type=NeuriteType.all, projection_plane="xy"): The circularity of the morphology points. """ projected_points = _unique_projected_points(morph, projection_plane, neurite_type) - return [] if len(projected_points) == 0 else morphmath.circularity(projected_points) + return np.nan if len(projected_points) == 0 else morphmath.circularity(projected_points) @feature(shape=()) @@ -683,4 +683,4 @@ def shape_factor(morph, neurite_type=NeuriteType.all, projection_plane="xy"): The shape factor of the morphology points. """ projected_points = _unique_projected_points(morph, projection_plane, neurite_type) - return [] if len(projected_points) == 0 else morphmath.shape_factor(projected_points) + return np.nan if len(projected_points) == 0 else morphmath.shape_factor(projected_points) diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index 20372b95..ae731323 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -916,6 +916,7 @@ def test_aspect_ratio(): 0.731076, decimal=6 ) + assert np.isnan(features.get("aspect_ratio", morph, neurite_type=nm.NeuriteType.custom5)) def test_circularity(): @@ -942,6 +943,7 @@ def test_circularity(): 0.730983, decimal=6 ) + assert np.isnan(features.get("circularity", morph, neurite_type=nm.NeuriteType.custom5)) def test_shape_factor(): @@ -968,3 +970,4 @@ def test_shape_factor(): 0.364678, decimal=6 ) + assert np.isnan(features.get("shape_factor", morph, neurite_type=nm.NeuriteType.custom5)) From f4cb0398ae72d8eec2f9e0d0102203b3a2c6e0ba Mon Sep 17 00:00:00 2001 From: Adrien Berchet Date: Fri, 8 Apr 2022 13:58:52 +0200 Subject: [PATCH 20/87] Fix: kwargs are unnecessarily updated in extract_stats (#1020) --- neurom/apps/morph_stats.py | 4 ++-- tests/apps/test_morph_stats.py | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/neurom/apps/morph_stats.py b/neurom/apps/morph_stats.py index 9cdd7de0..9bcb948c 100644 --- a/neurom/apps/morph_stats.py +++ b/neurom/apps/morph_stats.py @@ -36,6 +36,7 @@ import warnings from collections import defaultdict from collections.abc import Sized +from copy import deepcopy from functools import partial from itertools import chain, product from pathlib import Path @@ -93,7 +94,6 @@ def extract_dataframe(morphs, config, n_workers=1): """ if isinstance(morphs, Morphology): morphs = [morphs] - config = config.copy() func = partial(_run_extract_stats, config=config) if n_workers == 1: @@ -183,7 +183,7 @@ def extract_stats(morphs, config): for namespace, (feature_name, opts) in chain(neurite_features, morph_features, population_features): if isinstance(opts, dict): - kwargs = opts.get('kwargs', {}) + kwargs = deepcopy(opts.get('kwargs', {})) modes = opts.get('modes', []) else: kwargs = {} diff --git a/tests/apps/test_morph_stats.py b/tests/apps/test_morph_stats.py index ab76af17..6c15ef50 100644 --- a/tests/apps/test_morph_stats.py +++ b/tests/apps/test_morph_stats.py @@ -28,6 +28,7 @@ import os import warnings +from copy import deepcopy from pathlib import Path import neurom as nm @@ -155,8 +156,10 @@ def test_stats_new_format_set_arg(): 'soma_radius': {'modes': ['mean']}, } } + initial_config = deepcopy(config) res = ms.extract_stats(m, config) + assert config == initial_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'} @@ -180,12 +183,16 @@ def test_extract_stats_scalar_feature(): def test_extract_dataframe(): # Vanilla test + initial_config = deepcopy(REF_CONFIG_NEW) + morphs = nm.load_morphologies([SWC_PATH / 'Neuron.swc', SWC_PATH / 'simple.swc']) actual = ms.extract_dataframe(morphs, REF_CONFIG_NEW) + # drop raw features as they require too much test data to mock actual = actual.drop(columns='raw_section_branch_orders', level=1) expected = pd.read_csv(Path(DATA_PATH, 'extracted-stats.csv'), header=[0, 1], index_col=0) assert_frame_equal(actual, expected, check_dtype=False) + assert REF_CONFIG_NEW == initial_config # Test with a single morphology in the population morphs = nm.load_morphologies(SWC_PATH / 'Neuron.swc') @@ -193,35 +200,42 @@ def test_extract_dataframe(): # drop raw features as they require too much test data to mock actual = actual.drop(columns='raw_section_branch_orders', level=1) assert_frame_equal(actual, expected.iloc[[0]], check_dtype=False) + assert REF_CONFIG_NEW == initial_config # 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']} + initial_config = deepcopy(config) actual = ms.extract_dataframe(morphs, config) idx = pd.IndexSlice expected = expected.loc[:, idx[:, ['name', 'sum_section_lengths']]] assert_frame_equal(actual, expected, check_dtype=False) + assert config == initial_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) + assert config == initial_config # 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(morphs, config) assert_frame_equal(actual, expected, check_dtype=False) + assert config == initial_config # Test with a List[Path] argument 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) + assert config == initial_config # Test without any neurite_type keys, it should pick the defaults config = {'neurite': {'total_length_per_neurite': ['sum']}} + initial_config = deepcopy(config) actual = ms.extract_dataframe(morphs, config) expected_columns = pd.MultiIndex.from_tuples( [('property', 'name'), @@ -234,6 +248,35 @@ def test_extract_dataframe(): data=[['Neuron.swc', 207.87975221, 418.43241644, 214.37304578, 840.68521442], ['simple.swc', 15., 16., 0., 31., ]]) assert_frame_equal(actual, expected, check_dtype=False) + assert config == initial_config + + +def test_extract_dataframe_with_kwargs(): + config = { + 'neurite': { + 'section_lengths': {'kwargs': {'neurite_type': 'AXON'}, 'modes': ['max', 'sum']}, + }, + 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], + 'morphology': { + 'soma_radius': {'modes': ['mean']}, + } + } + initial_config = deepcopy(config) + + morphs = nm.load_morphologies([SWC_PATH / 'Neuron.swc', SWC_PATH / 'simple.swc']) + actual = ms.extract_dataframe(morphs, config) + + assert config == initial_config + + expected = pd.read_csv(Path(DATA_PATH, 'extracted-stats.csv'), header=[0, 1], index_col=0)[ + [ + ("property", "name"), + ("axon", "max_section_lengths"), + ("axon", "sum_section_lengths"), + ("morphology", "mean_soma_radius"), + ] + ] + assert_frame_equal(actual, expected, check_dtype=False) def test_extract_dataframe_multiproc(): From 1ecce98c73ac4da1be90a270b6eb63a77daf2f67 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 13 Apr 2022 12:42:25 +0200 Subject: [PATCH 21/87] Allow using lists of kwargs in morph_stats features (#1021) Makes possible to specify a list of kwargs in morph-stats to execute the same feature multiple times with different parameters. --- CHANGELOG.rst | 1 + doc/source/morph_stats.rst | 67 ++++++- neurom/apps/morph_stats.py | 240 ++++++++++++++++-------- tests/apps/test_morph_stats.py | 328 ++++++++++++++++++++++++++++++++- 4 files changed, 543 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8606975d..7a3dfef3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Version 3.2.0 ------------- +- List of multiple kwargs configurations are now allowed in``neurom.apps.morph_stats``. - ``neurom.features.neurite.principal_direction_extents`` directions correspond to extents ordered in a descending order. - Add features ``neurom.features.morphology.(aspect_ration, circularity, shape_factor)``` diff --git a/doc/source/morph_stats.rst b/doc/source/morph_stats.rst index 21201a3d..47917a43 100644 --- a/doc/source/morph_stats.rst +++ b/doc/source/morph_stats.rst @@ -58,7 +58,7 @@ Short format (prior version 3.0.0) An example config: .. code-block:: yaml - + neurite: section_lengths: - max @@ -67,13 +67,13 @@ An example config: - total section_branch_orders: - max - + neurite_type: - AXON - APICAL_DENDRITE - BASAL_DENDRITE - ALL - + neuron: soma_radius: - mean @@ -93,7 +93,7 @@ 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. It applies only to ``neurite`` features. A sample output using the above configuration: @@ -198,6 +198,65 @@ So the example config from `Short format (prior version 3.0.0)`_ looks: - mean +List of features format (starting version 3.2.0) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The kwargs entry was converted into a list to allow running the same feature with different arguments. The ``partition_asymmetry`` feature in the example above can be specified multiple times with different arguments as follows: + +.. code-block:: yaml + + neurite: + partition_asymmetry: + kwargs: + - method: petilla + variant: length + - method: uylings + variant: branch-order + modes: + - max + - sum + +To allow differentiation between the feature multiples, the keys and values of the kwargs are appended at the end of the feature name: + +.. code-block:: + + partition_asymmetry__variant:length__method:petilla + partition_asymmetry__variant:branch-order__method:uylings + +The example config from `Short format (prior version 3.0.0)`_ becomes: + +.. code-block:: yaml + + neurite: + section_branch_orders: + kwargs: + - {} + modes: + - max + section_lengths: + kwargs: + - {} + modes: + - max + - sum + section_volumes: + kwargs: + - {} + modes: + - sum + morphology: + soma_radius: + kwargs: + - {} + modes: + - mean + neurite_type: + - AXON + - APICAL_DENDRITE + - BASAL_DENDRITE + - ALL + + Features -------- diff --git a/neurom/apps/morph_stats.py b/neurom/apps/morph_stats.py index 9bcb948c..b1d90d9b 100644 --- a/neurom/apps/morph_stats.py +++ b/neurom/apps/morph_stats.py @@ -38,10 +38,8 @@ from collections.abc import Sized from copy import deepcopy from functools import partial -from itertools import chain, product from pathlib import Path import pkg_resources -from tqdm import tqdm import numpy as np import pandas as pd from morphio import SomaError @@ -53,7 +51,7 @@ 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 flatten, NeuromJSON, warn_deprecated +from neurom.utils import flatten, NeuromJSON L = logging.getLogger(__name__) @@ -76,9 +74,11 @@ def extract_dataframe(morphs, config, n_workers=1): 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: + - neurite: + Either a list of features: [feature_name, {kwargs: {}, modes: []}] or + a dictionary of features {feature_name: {kwargs: {}, modes: []}}. + - kwargs is an optional entry allowing to pass kwargs to the feature function + - modes is an aggregation operation provided as a string such as: ['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. @@ -90,7 +90,6 @@ def extract_dataframe(morphs, config, n_workers=1): Note: An example config can be found at: - {config_path} """ if isinstance(morphs, Morphology): morphs = [morphs] @@ -112,7 +111,7 @@ def extract_dataframe(morphs, config, n_workers=1): return pd.DataFrame(columns=pd.MultiIndex.from_tuples(columns), data=rows) -extract_dataframe.__doc__ = extract_dataframe.__doc__.format(config_path=EXAMPLE_CONFIG) +extract_dataframe.__doc__ += str(EXAMPLE_CONFIG) def _get_feature_stats(feature_name, morphs, modes, kwargs): @@ -120,6 +119,21 @@ def _get_feature_stats(feature_name, morphs, modes, kwargs): If the feature is 2-dimensional, the feature is flattened on its last axis """ + def stat_name_format(mode, feature_name, kwargs): + """Returns the key name for the data dictionary. + + The key is a combination of the mode, feature_name and an optional suffix of all the extra + kwargs that are passed in the feature function (apart from neurite_type). + """ + suffix = "__".join( + [f"{key}:{value}" for key, value in kwargs.items() if key != "neurite_type"] + ) + + if suffix: + return f"{mode}_{feature_name}__{suffix}" + + return f"{mode}_{feature_name}" + data = {} value, func = _get_feature_value_and_func(feature_name, morphs, **kwargs) shape = func.shape @@ -127,7 +141,8 @@ def _get_feature_stats(feature_name, morphs, modes, kwargs): 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_name = stat_name_format(mode, feature_name, kwargs) stat = value if isinstance(value, Sized): @@ -154,7 +169,12 @@ def extract_stats(morphs, config): 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: + Either a list of features: [feature_name, {kwargs: {}, modes: []}] or + a dictionary of features {feature_name: {kwargs: {}, modes: []}}. + - kwargs is an optional entry allowing to pass kwargs to the feature function + - modes is an aggregation operation provided as a string such as: + ['min', 'max', 'median', 'mean', 'std', 'raw', 'sum'] - 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', 'sum'] @@ -167,56 +187,60 @@ def extract_stats(morphs, config): Note: An example config can be found at: - {config_path} """ - 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()) + config = _sanitize_config(config) + 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 = deepcopy(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)) + stats = defaultdict(dict) + for category in ("neurite", "morphology", "population"): + for feature_name, opts in config[category].items(): + + list_of_kwargs = opts["kwargs"] + modes = opts["modes"] + + for feature_kwargs in list_of_kwargs: + + if category == 'neurite': + + # mutated below, need a copy + feature_kwargs = deepcopy(feature_kwargs) + + types = ( + neurite_types + if 'neurite_type' not in feature_kwargs and neurite_types + else [_NEURITE_MAP[feature_kwargs.get('neurite_type', 'ALL')]] + ) + + for neurite_type in types: + feature_kwargs["neurite_type"] = neurite_type + stats[neurite_type.name].update( + _get_feature_stats(feature_name, morphs, modes, feature_kwargs) + ) + + else: + stats[category].update( + _get_feature_stats(feature_name, morphs, modes, feature_kwargs) + ) return dict(stats) -extract_stats.__doc__ = extract_stats.__doc__.format(config_path=EXAMPLE_CONFIG) +extract_stats.__doc__ += str(EXAMPLE_CONFIG) -def get_header(results): +def _get_header(results): """Extracts the headers, using the first value in the dict as the template.""" - ret = ['name', ] values = next(iter(results.values())) - for k, v in values.items(): - for metric in v.keys(): - ret.append('%s:%s' % (k, metric)) - return ret + + return ['name'] + [ + f'{k}:{metric}' + for k, v in values.items() + for metric in v.keys() + ] -def generate_flattened_dict(headers, results): +def _generate_flattened_dict(headers, results): """Extract from results the fields in the headers list.""" for name, values in results.items(): row = [] @@ -224,7 +248,9 @@ def generate_flattened_dict(headers, results): if header == 'name': row.append(name) else: - neurite_type, metric = header.split(':') + # split on first occurence of `:` because feature kwargs may + # use a colon for separating key and value. + neurite_type, metric = header.split(':', 1) row.append(values[neurite_type][metric]) yield row @@ -240,24 +266,83 @@ def generate_flattened_dict(headers, results): 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 _NEURITE_FEATURES}, - 'morphology': {feature: modes for feature in _MORPHOLOGY_FEATURES}, - 'population': {feature: modes for feature in _POPULATION_FEATURES}, - 'neurite_type': list(_NEURITE_MAP.keys()), + + categories = { + "neurite": _NEURITE_FEATURES, + "morphology": _MORPHOLOGY_FEATURES, + "population": _POPULATION_FEATURES } + config = { + category: {name: {"kwargs": [{}], "modes": modes} for name in features} + for category, features in categories.items() + } + + config["neurite_type"] = list(_NEURITE_MAP.keys()) + + return config + + +def _standardize_layout(category_features): + """Standardizes the dictionary of features to a single format. + + Args: + category_features: A dictionary the keys of which are features names and its values are + either a list of modes ([]), a dictionary of kwargs and modes {kwargs: {}, modes: []}, or + the standardized layout where the kwargs take a list of dicts {kwargs: [{}], modes: []}. + + Returns: + The standardized features layout {feature: {kwargs: [{}], modes: []}} + + Notes: + And example of the final layout is: + + - feature1: + kwargs: + - kwargs1 + - kwargs2 + modes: + - mode1 + - mode2 + - feature2: + kwargs: + - kwargs1 + - kwargs2 + modes: + - mode1 + - mode2 + """ + def standardize_options(options): + """Returns options as a dict with two keys: 'kwargs' and 'modes'.""" + # convert short format + if isinstance(options, list): + return {"kwargs": [{}], "modes": options} + + modes = options.get("modes", []) -def sanitize_config(config): + if "kwargs" not in options: + return {"kwargs": [{}], "modes": modes} + + kwargs = options["kwargs"] + + # previous format where kwargs were a single entry + if isinstance(kwargs, dict): + return {"kwargs": [kwargs], "modes": modes} + + return {"kwargs": kwargs, "modes": modes} + + return {name: standardize_options(options) for name, options in category_features.items()} + + +def _sanitize_config(config): """Check that the config has the correct keys, add missing keys if necessary.""" - if 'neurite' in config: - if 'neurite_type' not in config: - raise ConfigError('"neurite_type" missing from config, but "neurite" set') - else: - config['neurite'] = {} + config = deepcopy(config) + + if "neuron" in config: + config["morphology"] = config.pop("neuron") - if 'morphology' not in config: - config['morphology'] = {} + for category in ("neurite", "morphology", "population"): + config[category] = _standardize_layout(config[category]) if category in config else {} return config @@ -273,28 +358,25 @@ def main(datapath, config, output_file, is_full_config, as_population, ignored_e as_population (bool): treat ``datapath`` as directory of morphologies population ignored_exceptions (list|tuple|None): exceptions to ignore when loading a morphology """ - if is_full_config: - config = full_config() - else: - try: - config = get_config(config, EXAMPLE_CONFIG) - config = sanitize_config(config) - except ConfigError as e: - L.error(e) - raise + config = full_config() if is_full_config else get_config(config, EXAMPLE_CONFIG) + + if 'neurite' in config and 'neurite_type' not in config: + error = ConfigError('"neurite_type" missing from config, but "neurite" set') + L.error(error) + raise error if ignored_exceptions is None: ignored_exceptions = () - ignored_exceptions = tuple(IGNORABLE_EXCEPTIONS[k] for k in ignored_exceptions) - morphs = nm.load_morphologies(get_files_by_path(datapath), - ignored_exceptions=ignored_exceptions) - results = {} + morphs = nm.load_morphologies( + get_files_by_path(datapath), + ignored_exceptions=tuple(IGNORABLE_EXCEPTIONS[k] for k in ignored_exceptions) + ) + if as_population: - results[datapath] = extract_stats(morphs, config) + results = {datapath: extract_stats(morphs, config)} else: - for m in tqdm(morphs): - results[m.name] = extract_stats(m, config) + results = {m.name: extract_stats(m, config) for m in morphs} if not output_file: print(json.dumps(results, indent=2, separators=(',', ':'), cls=NeuromJSON)) @@ -304,7 +386,7 @@ def main(datapath, config, output_file, is_full_config, as_population, ignored_e else: with open(output_file, 'w') as f: csvwriter = csv.writer(f) - header = get_header(results) + header = _get_header(results) csvwriter.writerow(header) - for line in generate_flattened_dict(header, dict(results)): + for line in _generate_flattened_dict(header, dict(results)): csvwriter.writerow(line) diff --git a/tests/apps/test_morph_stats.py b/tests/apps/test_morph_stats.py index 6c15ef50..1b1072a9 100644 --- a/tests/apps/test_morph_stats.py +++ b/tests/apps/test_morph_stats.py @@ -43,6 +43,7 @@ DATA_PATH = Path(__file__).parent.parent / 'data' SWC_PATH = DATA_PATH / 'swc' + REF_CONFIG = { 'neurite': { 'section_lengths': ['max', 'sum'], @@ -73,6 +74,8 @@ } } + + REF_OUT = { 'morphology': { 'mean_soma_radius': 0.13065629648763766, @@ -181,6 +184,122 @@ def test_extract_stats_scalar_feature(): 'morphology': {'sum_soma_volume': 1424.4383771584492}} + +def test_extract_stats__kwarg_modes_multiple_features(): + + m = nm.load_morphology(SWC_PATH / 'Neuron.swc') + config = { + 'neurite': { + 'principal_direction_extents': { + 'kwargs': [ + {"direction": 2}, + {"direction": 1}, + {"direction": 0}, + ], + 'modes': ['sum', "min"] + }, + }, + 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], + 'morphology': { + 'soma_radius': {'modes': ['mean']}, + 'partition_asymmetry': { + 'kwargs': [ + {'variant': 'branch-order', 'method': 'petilla'}, + {'variant': 'length', 'method': 'uylings'}, + ], + 'modes': ['min', 'max'], + }, + } + } + + res = ms.extract_stats(m, config) + + assert set(res.keys()) == {"axon", "basal_dendrite", "apical_dendrite", "all", "morphology"} + + for key in ("axon", "basal_dendrite", "apical_dendrite", "all"): + + assert set(res[key].keys()) == { + "sum_principal_direction_extents__direction:2", + "min_principal_direction_extents__direction:2", + "sum_principal_direction_extents__direction:1", + "min_principal_direction_extents__direction:1", + "sum_principal_direction_extents__direction:0", + "min_principal_direction_extents__direction:0", + } + + assert set(res["morphology"].keys()) == { + "mean_soma_radius", + "min_partition_asymmetry__variant:branch-order__method:petilla", + "max_partition_asymmetry__variant:branch-order__method:petilla", + "min_partition_asymmetry__variant:length__method:uylings", + "max_partition_asymmetry__variant:length__method:uylings", + } + + +def test_extract_dataframe__kwarg_modes_multiple_features(): + m = nm.load_morphology(SWC_PATH / 'Neuron.swc') + config = { + 'neurite': { + 'principal_direction_extents': { + 'kwargs': [ + {"direction": 2}, + {"direction": 1}, + {"direction": 0}, + ], + 'modes': ['sum', "min"], + }, + }, + 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], + 'morphology': { + 'soma_radius': {'modes': ['mean']}, + 'partition_asymmetry': { + 'kwargs': [ + {'variant': 'branch-order', 'method': 'petilla'}, + {'variant': 'length', 'method': 'uylings'}, + ], + 'modes': ['min', 'max'], + }, + }, + } + + res = ms.extract_dataframe(m, config) + + expected_columns = pd.MultiIndex.from_tuples([ + ('property', 'name'), + ('axon', 'sum_principal_direction_extents__direction:2'), + ('axon', 'min_principal_direction_extents__direction:2'), + ('axon', 'sum_principal_direction_extents__direction:1'), + ('axon', 'min_principal_direction_extents__direction:1'), + ('axon', 'sum_principal_direction_extents__direction:0'), + ('axon', 'min_principal_direction_extents__direction:0'), + ('apical_dendrite', 'sum_principal_direction_extents__direction:2'), + ('apical_dendrite', 'min_principal_direction_extents__direction:2'), + ('apical_dendrite', 'sum_principal_direction_extents__direction:1'), + ('apical_dendrite', 'min_principal_direction_extents__direction:1'), + ('apical_dendrite', 'sum_principal_direction_extents__direction:0'), + ('apical_dendrite', 'min_principal_direction_extents__direction:0'), + ('basal_dendrite', 'sum_principal_direction_extents__direction:2'), + ('basal_dendrite', 'min_principal_direction_extents__direction:2'), + ('basal_dendrite', 'sum_principal_direction_extents__direction:1'), + ('basal_dendrite', 'min_principal_direction_extents__direction:1'), + ('basal_dendrite', 'sum_principal_direction_extents__direction:0'), + ('basal_dendrite', 'min_principal_direction_extents__direction:0'), + ('all', 'sum_principal_direction_extents__direction:2'), + ('all', 'min_principal_direction_extents__direction:2'), + ('all', 'sum_principal_direction_extents__direction:1'), + ('all', 'min_principal_direction_extents__direction:1'), + ('all', 'sum_principal_direction_extents__direction:0'), + ('all', 'min_principal_direction_extents__direction:0'), + ('morphology', 'mean_soma_radius'), + ('morphology', 'min_partition_asymmetry__variant:branch-order__method:petilla'), + ('morphology', 'max_partition_asymmetry__variant:branch-order__method:petilla'), + ('morphology', 'min_partition_asymmetry__variant:length__method:uylings'), + ('morphology', 'max_partition_asymmetry__variant:length__method:uylings'), + ]) + + pd.testing.assert_index_equal(res.columns, expected_columns) + + def test_extract_dataframe(): # Vanilla test initial_config = deepcopy(REF_CONFIG_NEW) @@ -279,6 +398,8 @@ def test_extract_dataframe_with_kwargs(): assert_frame_equal(actual, expected, check_dtype=False) + + def test_extract_dataframe_multiproc(): morphs = [Path(SWC_PATH, name) for name in ['Neuron.swc', 'simple.swc']] @@ -303,23 +424,152 @@ def test_get_header(): 'fake_name1': REF_OUT, 'fake_name2': REF_OUT, } - header = ms.get_header(fake_results) + header = ms._get_header(fake_results) + assert 1 + 2 + 4 * (4 + 5) == len(header) # name + everything in REF_OUT assert 'name' in header assert 'morphology:mean_soma_radius' in header +def test_get_header__with_kwargs(): + + fake_results = { + "fake_name0": { + 'axon': { + 'sum_principal_direction_extents__direction:2': 4.236138323156951, + 'min_principal_direction_extents__direction:2': 4.236138323156951, + 'sum_principal_direction_extents__direction:1': 8.070668782620396, + 'max_principal_direction_extents__direction:1': 8.070668782620396, + 'mean_principal_direction_extents__direction:0': 82.38543140446015 + }, + 'apical_dendrite': { + 'sum_principal_direction_extents__direction:2': 3.6493184467335213, + 'min_principal_direction_extents__direction:2': 3.6493184467335213, + 'sum_principal_direction_extents__direction:1': 5.5082642304864695, + 'max_principal_direction_extents__direction:1': 5.5082642304864695, + 'mean_principal_direction_extents__direction:0': 99.57940514500457 + }, + 'basal_dendrite': { + 'sum_principal_direction_extents__direction:2': 7.32638745131256, + 'min_principal_direction_extents__direction:2': 3.10141343122575, + 'sum_principal_direction_extents__direction:1': 11.685447149154676, + 'max_principal_direction_extents__direction:1': 6.410958014733595, + 'mean_principal_direction_extents__direction:0': 87.2112016874677 + }, + 'all': { + 'sum_principal_direction_extents__direction:2': 15.211844221203034, + 'min_principal_direction_extents__direction:2': 3.10141343122575, + 'sum_principal_direction_extents__direction:1': 25.26438016226154, + 'max_principal_direction_extents__direction:1': 8.070668782620396, + 'mean_principal_direction_extents__direction:0': 89.09680998110002 + }, + 'morphology': { + 'mean_soma_radius': 0.13065629977308288, + 'min_partition_asymmetry__variant:branch-order__method:petilla': 0.0, + 'max_partition_asymmetry__variant:branch-order__method:petilla': 0.9, + 'min_partition_asymmetry__variant:length__method:uylings': 0.00030289197373727377, + 'max_partition_asymmetry__variant:length__method:uylings': 0.8795344229855895} + } + } + + assert ms._get_header(fake_results) == [ + 'name', + 'axon:sum_principal_direction_extents__direction:2', + 'axon:min_principal_direction_extents__direction:2', + 'axon:sum_principal_direction_extents__direction:1', + 'axon:max_principal_direction_extents__direction:1', + 'axon:mean_principal_direction_extents__direction:0', + 'apical_dendrite:sum_principal_direction_extents__direction:2', + 'apical_dendrite:min_principal_direction_extents__direction:2', + 'apical_dendrite:sum_principal_direction_extents__direction:1', + 'apical_dendrite:max_principal_direction_extents__direction:1', + 'apical_dendrite:mean_principal_direction_extents__direction:0', + 'basal_dendrite:sum_principal_direction_extents__direction:2', + 'basal_dendrite:min_principal_direction_extents__direction:2', + 'basal_dendrite:sum_principal_direction_extents__direction:1', + 'basal_dendrite:max_principal_direction_extents__direction:1', + 'basal_dendrite:mean_principal_direction_extents__direction:0', + 'all:sum_principal_direction_extents__direction:2', + 'all:min_principal_direction_extents__direction:2', + 'all:sum_principal_direction_extents__direction:1', + 'all:max_principal_direction_extents__direction:1', + 'all:mean_principal_direction_extents__direction:0', + 'morphology:mean_soma_radius', + 'morphology:min_partition_asymmetry__variant:branch-order__method:petilla', + 'morphology:max_partition_asymmetry__variant:branch-order__method:petilla', + 'morphology:min_partition_asymmetry__variant:length__method:uylings', + 'morphology:max_partition_asymmetry__variant:length__method:uylings' + ] + + def test_generate_flattened_dict(): fake_results = {'fake_name0': REF_OUT, 'fake_name1': REF_OUT, 'fake_name2': REF_OUT, } - header = ms.get_header(fake_results) - rows = list(ms.generate_flattened_dict(header, fake_results)) + header = ms._get_header(fake_results) + rows = list(ms._generate_flattened_dict(header, fake_results)) assert 3 == len(rows) # one for fake_name[0-2] assert 1 + 2 + 4 * (4 + 5) == len(rows[0]) # name + everything in REF_OUT +def test_generate_flattened_dict__with_kwargs(): + + results = { + 'axon': { + 'sum_principal_direction_extents__direction:2': 0.0, + 'min_principal_direction_extents__direction:2': 1.0, + 'sum_principal_direction_extents__direction:1': 2.0, + 'max_principal_direction_extents__direction:1': 3.0, + 'mean_principal_direction_extents__direction:0': 4.0, + }, + 'apical_dendrite': { + 'sum_principal_direction_extents__direction:2': 5.0, + 'min_principal_direction_extents__direction:2': 6.0, + 'sum_principal_direction_extents__direction:1': 7.0, + 'max_principal_direction_extents__direction:1': 8.0, + 'mean_principal_direction_extents__direction:0': 9.0, + }, + 'basal_dendrite': { + 'sum_principal_direction_extents__direction:2': 1.0, + 'min_principal_direction_extents__direction:2': 2.0, + 'sum_principal_direction_extents__direction:1': 3.0, + 'max_principal_direction_extents__direction:1': 4.0, + 'mean_principal_direction_extents__direction:0': 5.0, + }, + 'all': { + 'sum_principal_direction_extents__direction:2': 6.0, + 'min_principal_direction_extents__direction:2': 7.0, + 'sum_principal_direction_extents__direction:1': 8.0, + 'max_principal_direction_extents__direction:1': 9.0, + 'mean_principal_direction_extents__direction:0': 1.0, + }, + 'morphology': { + 'mean_soma_radius': 2.0, + 'min_partition_asymmetry__variant:branch-order__method:petilla': 3.0, + 'max_partition_asymmetry__variant:branch-order__method:petilla': 4.0, + 'min_partition_asymmetry__variant:length__method:uylings': 5.0, + 'max_partition_asymmetry__variant:length__method:uylings': 6.0, + } + } + + fake_results = { + "fake_name0": results, + "fake_name1": results, + } + + header = ms._get_header(fake_results) + + assert list(ms._generate_flattened_dict(header, fake_results)) == [ + [ + 'fake_name0', 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, 9.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + [ + 'fake_name1', 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, 9.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0] + ] + + def test_full_config(): config = ms.full_config() assert set(config.keys()) == {'neurite', 'population', 'morphology', 'neurite_type'} @@ -329,13 +579,51 @@ def test_full_config(): assert set(config['population'].keys()) == set(_POPULATION_FEATURES.keys()) -def test_sanitize_config(): +def test_standardize_layout(): + """Converts the config category entries (e.g. neurite, morphology, population) to using + the kwarg and modes layout. + """ + # from short format + entry = {"f1": ["min", "max"], "f2": ["min"], "f3": []} + assert ms._standardize_layout(entry) == { + "f1": {"kwargs": [{}], "modes": ["min", "max"]}, + "f2": {"kwargs": [{}], "modes": ["min"]}, + "f3": {"kwargs": [{}], "modes": []}, + } + + # from kwarg/modes with missing options + entry = { + "f1": {"kwargs": {"a1": 1, "a2": 2}, "modes": ["min", "max"]}, + "f2": {"modes": ["min", "median"]}, + "f3": {"kwargs": {"a1": 1, "a2": 2}}, + "f4": {}, + } + assert ms._standardize_layout(entry) == { + "f1": {"kwargs": [{"a1": 1, "a2": 2}], "modes": ["min", "max"]}, + "f2": {"kwargs": [{}], "modes": ["min", "median"]}, + "f3": {"kwargs": [{"a1": 1, "a2": 2}], "modes": []}, + "f4": {"kwargs": [{}], "modes": []}, + } + + # from list of kwargs format + entry = { + "f1": {"kwargs": [{"a1": 1, "a2": 2}], "modes": ["min", "max"]}, + "f2": {"modes": ["min", "median"]}, + "f3": {"kwargs": [{"a1": 1, "a2": 2}]}, + "f4": {}, + } + assert ms._standardize_layout(entry) == { + "f1": {"kwargs": [{"a1": 1, "a2": 2}], "modes": ["min", "max"]}, + "f2": {"kwargs": [{}], "modes": ["min", "median"]}, + "f3": {"kwargs": [{"a1": 1, "a2": 2}], "modes": []}, + "f4": {"kwargs": [{}], "modes": []}, + } + - with pytest.raises(ConfigError): - ms.sanitize_config({'neurite': []}) +def test_sanitize_config(): - new_config = ms.sanitize_config({}) # empty - assert 2 == len(new_config) # neurite & morphology created + new_config = ms._sanitize_config({}) # empty + assert 3 == len(new_config) # neurite & morphology & population created full_config = { 'neurite': { @@ -348,8 +636,28 @@ def test_sanitize_config(): 'soma_radius': ['mean'] } } - new_config = ms.sanitize_config(full_config) - assert 3 == len(new_config) # neurite, neurite_type & morphology + new_config = ms._sanitize_config(full_config) + + expected_config = { + 'neurite': { + 'section_lengths': {"kwargs": [{}], "modes": ['max', 'sum']}, + 'section_volumes': {"kwargs": [{}], "modes": ['sum']}, + 'section_branch_orders': {"kwargs": [{}], "modes": ['max']}, + }, + 'neurite_type': ['AXON', 'APICAL_DENDRITE', 'BASAL_DENDRITE', 'ALL'], + 'morphology': { + 'soma_radius': {"kwargs": [{}], "modes": ["mean"]}, + }, + "population": {}, + } + assert new_config == expected_config + + # check that legacy neuron entries are converted to morphology ones + full_config["neuron"] = full_config.pop("morphology") + assert ms._sanitize_config(full_config) == expected_config + + # check that all formats are converted to the same sanitized config: + assert ms._sanitize_config(REF_CONFIG) == ms._sanitize_config(REF_CONFIG_NEW) def test_multidimensional_features(): From dd28360a87fea9a1648e38ec555b9624d9ba6f6f Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 14 Apr 2022 11:07:16 +0200 Subject: [PATCH 22/87] Implement new feature of length above soma (#1023) --- CHANGELOG.rst | 1 + neurom/features/morphology.py | 38 +++++++++++++++++++++++++++++ tests/features/test_get_features.py | 31 +++++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7a3dfef3..3113b9a1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Version 3.2.0 ------------- +- Add ``neurom.features.morphology.length_fraction_above_soma`` feature. - List of multiple kwargs configurations are now allowed in``neurom.apps.morph_stats``. - ``neurom.features.neurite.principal_direction_extents`` directions correspond to extents ordered in a descending order. diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index bc7e096a..fc387a07 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -684,3 +684,41 @@ def shape_factor(morph, neurite_type=NeuriteType.all, projection_plane="xy"): """ projected_points = _unique_projected_points(morph, projection_plane, neurite_type) return np.nan if len(projected_points) == 0 else morphmath.shape_factor(projected_points) + + +@feature(shape=()) +def length_fraction_above_soma(morph, neurite_type=NeuriteType.all, up="Y"): + """Returns the length fraction of the segments that have their midpoints higher than the soma. + + Args: + morph: Morphology object. + neurite_type: The neurite type to use. By default all neurite types are used. + up: The axis along which the computation is performed. One of ('X', 'Y', 'Z'). + + Returns: + The fraction of neurite length that lies on the right of the soma along the given axis. + """ + axis = up.upper() + + if axis not in {"X", "Y", "Z"}: + raise NeuroMError(f"Unknown axis {axis}. Please choose 'X', 'Y', or 'Z'.") + + col = getattr(COLS, axis) + segments = list(iter_segments(morph, neurite_filter=is_type(neurite_type))) + + if not segments: + return np.nan + + # (Segment 1, Segment 2) x (X, Y, Z, R) X N + segments = np.dstack(segments) + + # shape N x 3 + seg_begs = segments[0, COLS.XYZ, :].T + seg_ends = segments[1, COLS.XYZ, :].T + + lengths = np.linalg.norm(seg_begs - seg_ends, axis=1) + + midpoints = 0.5 * (seg_begs + seg_ends) + selection = midpoints[:, col] > morph.soma.center[col] + + return lengths[selection].sum() / lengths.sum() diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index ae731323..b0ac1be0 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -971,3 +971,34 @@ def test_shape_factor(): decimal=6 ) assert np.isnan(features.get("shape_factor", morph, neurite_type=nm.NeuriteType.custom5)) + + +@pytest.mark.parametrize("neurite_type, axis, expected_value", [ + (nm.AXON, "X", 0.50), + (nm.AXON, "Y", 0.74), + (nm.AXON, "Z", 0.16), + (nm.APICAL_DENDRITE, "X", np.nan), + (nm.APICAL_DENDRITE, "Y", np.nan), + (nm.APICAL_DENDRITE, "Z", np.nan), + (nm.BASAL_DENDRITE, "X", 0.50), + (nm.BASAL_DENDRITE, "Y", 0.59), + (nm.BASAL_DENDRITE, "Z", 0.48), +] +) +def test_length_fraction_from_soma(neurite_type, axis, expected_value): + + morph = load_morphology(DATA_PATH / "neurolucida/bio_neuron-000.asc") + + npt.assert_almost_equal( + features.get("length_fraction_above_soma", morph, neurite_type=neurite_type, up=axis), + expected_value, + decimal=2 + ) + + +def test_length_fraction_from_soma__wrong_axis(): + + morph = load_morphology(DATA_PATH / "neurolucida/bio_neuron-000.asc") + + with pytest.raises(NeuroMError): + features.get("length_fraction_above_soma", morph, up='K') From c6d187a1966ca82553e2a98c2c6e36a6b392299f Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 14 Apr 2022 19:02:21 +0200 Subject: [PATCH 23/87] Exit early if not points, to avoid hull warning (#1024) --- neurom/features/morphology.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index fc387a07..ddee3aa1 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -596,6 +596,9 @@ def volume_density(morph, neurite_type=NeuriteType.all): for point in point_list ] + if not points: + return np.nan + morph_hull = convex_hull(points) if morph_hull is None: From b99aa710bfcd6fdb5d501a025d4e807a1eedf7ff Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 14 Feb 2022 13:12:12 +0100 Subject: [PATCH 24/87] Subtree processing example --- neurom/features/__init__.py | 7 +++- neurom/features/morphology.py | 56 ++++++++++++++++++++++++++---- neurom/features/neurite.py | 11 +++--- tests/test_mixed.py | 64 +++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 tests/test_mixed.py diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 924de8d1..451a185a 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -90,6 +90,11 @@ def _get_feature_value_and_func(feature_name, obj, **kwargs): raise NeuroMError('Only Neurite, Morphology, Population or list, tuple of Neurite,' ' Morphology can be used for feature calculation') + use_subtrees = False + if "use_subtrees" in kwargs: + use_subtrees = kwargs["use_subtrees"] + del kwargs["use_subtrees"] + neurite_filter = is_type(kwargs.get('neurite_type', NeuriteType.all)) res, feature_ = None, None @@ -107,7 +112,7 @@ def _get_feature_value_and_func(feature_name, obj, **kwargs): # input is a morphology if feature_name in _MORPHOLOGY_FEATURES: feature_ = _MORPHOLOGY_FEATURES[feature_name] - res = feature_(obj, **kwargs) + res = feature_(obj, use_subtrees=use_subtrees, **kwargs) elif feature_name in _NEURITE_FEATURES: feature_ = _NEURITE_FEATURES[feature_name] res = _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index ddee3aa1..f1117ba1 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -59,14 +59,58 @@ from neurom.utils import str_to_plane from neurom.morphmath import convex_hull +from neurom.core.morphology import Neurite +from collections import OrderedDict feature = partial(feature, namespace=NameSpace.NEURON) -def _map_neurites(function, morph, neurite_type): - return list( - iter_neurites(morph, mapfun=function, filt=is_type(neurite_type)) - ) +def _homogeneous_subtrees(neurite): + """Returns a dictionary the keys of which are section types and the values are the + sub-neurites. A sub-neurite can be either the entire tree or a homogeneous downstream + sub-tree. + + Note: Only two different mixed types are allowed + """ + homogeneous_neurites = OrderedDict([(neurite.root_node.type, neurite)]) + for section in neurite.root_node.ipreorder(): + if section.type not in homogeneous_neurites: + homogeneous_neurites[section.type] = Neurite(section) + if len(homogeneous_neurites) != 2: + raise TypeError( + f"Subtree types must be exactly two. Found {len(homogeneous_neurites)} instead.") + return homogeneous_neurites + + +def _map_homogeneous_subtrees(function, neurite, neurite_type): + + check_type = is_type(neurite_type) + for neurite_type, sub_neurite in _homogeneous_subtrees(neurite).items(): + if check_type(sub_neurite): + yield function(sub_neurite, section_type=neurite_type) + + +def _map_neurites(function, morph, neurite_type=NeuriteType.all, use_subtrees=False): + """ + If `use_subtrees` is enabled, each neurite that is inhomogeneous, is traversed and the + subtrees with their respective types are returned. For each of these subtrees the features + need to be calculated with a section filter, to ensure no sections from other subtrees are + traversed. + """ + if use_subtrees: + # we don't know a priori the types of the subtrees in the neurites, thus we cannot + # filter by time at the neurite level yet + for neurite in iter_neurites(morph): + + # map the function for each subtree with homogeneous type, propagating the type + # for a section level filtering + if neurite.morphio_root_node.is_heterogeneous(): + yield from _map_homogeneous_subtrees(function, neurite, neurite_type) + else: + if istype(neurite_type)(neurite): + yield function(neurite) + else: + yield from iter_neurites(morph, mapfun=function, filt=is_type(neurite_type)) @feature(shape=()) @@ -100,9 +144,9 @@ def max_radial_distance(morph, neurite_type=NeuriteType.all): @feature(shape=(...,)) -def number_of_sections_per_neurite(morph, neurite_type=NeuriteType.all): +def number_of_sections_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """List of numbers of sections per neurite.""" - return _map_neurites(nf.number_of_sections, morph, neurite_type) + return list(_map_neurites(nf.number_of_sections, morph, neurite_type, use_subtrees)) @feature(shape=(...,)) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 6aac5e9f..418b41bf 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -48,19 +48,22 @@ import numpy as np from neurom import morphmath +from neurom.core.types import NeuriteType 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.morphmath import convex_hull +from neurom.core.types import tree_type_checker as is_type + feature = partial(feature, namespace=NameSpace.NEURITE) L = logging.getLogger(__name__) -def _map_sections(fun, neurite, iterator_type=Section.ipreorder): +def _map_sections(fun, neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Map `fun` to all the sections.""" - return list(map(fun, iterator_type(neurite.root_node))) + return list(map(fun, filter(is_type(section_type), iterator_type(neurite.root_node)))) @feature(shape=()) @@ -77,9 +80,9 @@ def number_of_segments(neurite): @feature(shape=()) -def number_of_sections(neurite, iterator_type=Section.ipreorder): +def number_of_sections(neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Number of sections. For a morphology it will be a sum of all neurites sections numbers.""" - return len(_map_sections(lambda s: s, neurite, iterator_type=iterator_type)) + return len(_map_sections(lambda s: s, neurite, iterator_type=iterator_type, section_type=section_type)) @feature(shape=()) diff --git a/tests/test_mixed.py b/tests/test_mixed.py new file mode 100644 index 00000000..f6a305e1 --- /dev/null +++ b/tests/test_mixed.py @@ -0,0 +1,64 @@ +import pytest +import neurom +from neurom import NeuriteType +from neurom.features import get + +@pytest.fixture +def mixed_morph(): + return neurom.load_morphology( + """ + 1 1 0 0 0 1.0 -1 + 2 3 0 1 0 2.2 1 + 3 3 1 2 0 2.2 2 + 4 3 1 4 0 2.2 3 + 5 2 2 3 0 2.2 3 + 6 2 2 4 0 2.2 5 + 7 2 3 3 0 2.1 5 + """, + reader="swc") + + +def test_morph_number_of_sections_per_neurite(mixed_morph): + assert get("number_of_sections_per_neurite", mixed_morph, use_subtrees=False) == [5] + assert get("number_of_sections_per_neurite", mixed_morph, use_subtrees=True) == [2, 3] + + assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.basal_dendrite, use_subtrees=False) == [5] + assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.basal_dendrite, use_subtrees=True) == [2] + + assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.axon, use_subtrees=False) == [] + assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.axon, use_subtrees=True) == [3] + +""" +def test_mixed__segment_lengths(mixed_morph): + + axon_on_dendrite = mixed_morph.neurites[0] + + get("segment_lengths", process_inhomogeneous_subtrees=True, neurite_type=NeuriteType.axon) + +def test_features(mixed_morph): + + # the traditional way processes each tree as a whole + assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=False) == 1 + assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=False, neurite_type=NeuriteType.basal_dendrite) == 1 + assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=False, neurite_type=NeuriteType.axon) == 0 + + # the new way checks for inhomogeneous subtrees anc counts them as separate neurites + assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=True) == 2 + assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=True, neurite_type=NeuriteType.basal_dendrite) == 1 + assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=True, neurite_type=NeuriteType.axon) == 1 + + + + +def test_mixed_types(mixed_morph): + + from neurom import NeuriteType + from neurom.features import get + + types = [neurite.type for neurite in mixed_morph.neurites] + + res = get("number_of_sections", mixed_morph, neurite_type=NeuriteType.axon) + + print(types, res) + assert False +""" From 7338d4525456a6a19521febb6deed517cad2795d Mon Sep 17 00:00:00 2001 From: Mike Gevaert Date: Wed, 16 Feb 2022 09:07:23 +0100 Subject: [PATCH 25/87] test original path --- neurom/features/morphology.py | 2 +- tests/features/test_get_features.py | 44 ++++++++++++++++++----------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index f1117ba1..e6004cbf 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -107,7 +107,7 @@ def _map_neurites(function, morph, neurite_type=NeuriteType.all, use_subtrees=Fa if neurite.morphio_root_node.is_heterogeneous(): yield from _map_homogeneous_subtrees(function, neurite, neurite_type) else: - if istype(neurite_type)(neurite): + if is_type(neurite_type)(neurite): yield function(neurite) else: yield from iter_neurites(morph, mapfun=function, filt=is_type(neurite_type)) diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index b0ac1be0..65ab30d5 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -644,23 +644,33 @@ def test_section_radial_distances_origin(): 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]) - - nsecs = features.get('number_of_sections_per_neurite', NEURON, neurite_type=NeuriteType.axon) - assert len(nsecs) == 1 - assert nsecs == [21] - - nsecs = features.get('number_of_sections_per_neurite', NEURON, - neurite_type=NeuriteType.basal_dendrite) - assert len(nsecs) == 2 - assert np.all(nsecs == [21, 21]) - - nsecs = features.get('number_of_sections_per_neurite', NEURON, - neurite_type=NeuriteType.apical_dendrite) - assert len(nsecs) == 1 - assert np.all(nsecs == [21]) + for use_subtrees in (True, False): + nsecs = features.get('number_of_sections_per_neurite', + NEURON, + use_subtrees=use_subtrees) + assert len(nsecs) == 4 + assert np.all(nsecs == [21, 21, 21, 21]) + + nsecs = features.get('number_of_sections_per_neurite', + NEURON, + neurite_type=NeuriteType.axon, + use_subtrees=use_subtrees) + assert len(nsecs) == 1 + assert nsecs == [21] + + nsecs = features.get('number_of_sections_per_neurite', + NEURON, + neurite_type=NeuriteType.basal_dendrite, + use_subtrees=use_subtrees) + assert len(nsecs) == 2 + assert np.all(nsecs == [21, 21]) + + nsecs = features.get('number_of_sections_per_neurite', + NEURON, + neurite_type=NeuriteType.apical_dendrite, + use_subtrees=use_subtrees) + assert len(nsecs) == 1 + assert np.all(nsecs == [21]) def test_trunk_origin_radii(): From ca468610748e812a62c1ecf2196be974d23f8832 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 16 Feb 2022 15:33:43 +0100 Subject: [PATCH 26/87] Make use_subtrees explicit argument. --- neurom/features/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 451a185a..4865dcdf 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -72,7 +72,7 @@ def _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs): 0 if feature_.shape == () else []) -def _get_feature_value_and_func(feature_name, obj, **kwargs): +def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs): """Obtain a feature from a set of morphology objects. Arguments: @@ -90,11 +90,6 @@ def _get_feature_value_and_func(feature_name, obj, **kwargs): raise NeuroMError('Only Neurite, Morphology, Population or list, tuple of Neurite,' ' Morphology can be used for feature calculation') - use_subtrees = False - if "use_subtrees" in kwargs: - use_subtrees = kwargs["use_subtrees"] - del kwargs["use_subtrees"] - neurite_filter = is_type(kwargs.get('neurite_type', NeuriteType.all)) res, feature_ = None, None From 096485ff4b8e84efe23b856293acb611cbf08e3c Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 16 Feb 2022 15:34:15 +0100 Subject: [PATCH 27/87] Use a regular dict --- neurom/features/morphology.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index e6004cbf..1a3bc7fb 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -60,7 +60,6 @@ from neurom.morphmath import convex_hull from neurom.core.morphology import Neurite -from collections import OrderedDict feature = partial(feature, namespace=NameSpace.NEURON) @@ -72,7 +71,7 @@ def _homogeneous_subtrees(neurite): Note: Only two different mixed types are allowed """ - homogeneous_neurites = OrderedDict([(neurite.root_node.type, neurite)]) + homogeneous_neurites = {neurite.root_node.type: neurite} for section in neurite.root_node.ipreorder(): if section.type not in homogeneous_neurites: homogeneous_neurites[section.type] = Neurite(section) From 5a558ab7b9b5640df71bd6626e8fe49dc7c44b6d Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 16 Feb 2022 15:35:29 +0100 Subject: [PATCH 28/87] Make explicit use_subtrees in get --- neurom/features/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 4865dcdf..0a4634e7 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -132,7 +132,7 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) return res, feature_ -def get(feature_name, obj, **kwargs): +def get(feature_name, obj, use_subtrees=False, **kwargs): """Obtain a feature from a set of morphology objects. Features can be either Neurite, Morphology or Population features. For Neurite features see @@ -147,7 +147,7 @@ def get(feature_name, obj, **kwargs): Returns: List|Number: feature value as a list or a single number. """ - return _get_feature_value_and_func(feature_name, obj, **kwargs)[0] + return _get_feature_value_and_func(feature_name, obj, use_subtrees=use_subtrees, **kwargs)[0] def _register_feature(namespace: NameSpace, name, func, shape): From 325c23552ab439bebe0b4765848cf14b633987c0 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 16 Feb 2022 18:06:15 +0100 Subject: [PATCH 29/87] Make mixed morphology more complex --- tests/test_mixed.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index f6a305e1..a823889e 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -3,31 +3,49 @@ from neurom import NeuriteType from neurom.features import get + @pytest.fixture def mixed_morph(): + """ + basal_dendrite: homogeneous + axon_on_basal_dendrite: heterogeneous + apical_dendrite: homogeneous + """ return neurom.load_morphology( """ - 1 1 0 0 0 1.0 -1 - 2 3 0 1 0 2.2 1 - 3 3 1 2 0 2.2 2 - 4 3 1 4 0 2.2 3 - 5 2 2 3 0 2.2 3 - 6 2 2 4 0 2.2 5 - 7 2 3 3 0 2.1 5 + 1 1 0 0 0 0.5 -1 + 2 3 -1 0 0 0.1 1 + 3 3 -2 0 0 0.1 2 + 4 3 0 -3 0 0.1 3 + 5 3 -2 1 0 0.1 3 + 6 3 0 1 0 0.1 1 + 7 3 1 2 0 0.1 6 + 8 3 1 4 0 0.1 7 + 9 2 2 3 0 0.1 7 + 10 2 2 4 0 0.1 9 + 11 2 3 3 0 0.1 9 + 12 4 0 -1 0 0.1 1 + 13 4 0 -2 0 0.1 12 + 14 4 0 -3 0 0.1 13 + 15 4 1 -2 0 0.1 13 """, reader="swc") def test_morph_number_of_sections_per_neurite(mixed_morph): - assert get("number_of_sections_per_neurite", mixed_morph, use_subtrees=False) == [5] - assert get("number_of_sections_per_neurite", mixed_morph, use_subtrees=True) == [2, 3] - assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.basal_dendrite, use_subtrees=False) == [5] - assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.basal_dendrite, use_subtrees=True) == [2] + assert get("number_of_sections_per_neurite", mixed_morph, use_subtrees=False) == [3, 5, 3] + assert get("number_of_sections_per_neurite", mixed_morph, use_subtrees=True) == [3, 2, 3, 3] + + assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.basal_dendrite, use_subtrees=False) == [3, 5] + assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.basal_dendrite, use_subtrees=True) == [3, 2] assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.axon, use_subtrees=False) == [] assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.axon, use_subtrees=True) == [3] + assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.apical_dendrite, use_subtrees=False) == [3] + assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.apical_dendrite, use_subtrees=True) == [3] + """ def test_mixed__segment_lengths(mixed_morph): From a6f881e1c4ee9d4cd890be4b5197646889521ad8 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 20 Feb 2022 01:13:22 +0100 Subject: [PATCH 30/87] Add is_heterogeneous to Neurite --- neurom/core/morphology.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 1a8404a8..6ba333e7 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -386,6 +386,10 @@ def volume(self): """ return sum(s.volume for s in self.iter_sections()) + def is_heterogeneous(self) -> bool: + """Returns true if the neurite consists of more that one section types""" + return self.morphio_root_node.is_heterogeneous() + def iter_sections(self, order=Section.ipreorder, neurite_order=NeuriteIter.FileOrder): """Iteration over section nodes. From 6ef838da475b930abb2117735a8c049ec97fb71d Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 20 Feb 2022 01:17:27 +0100 Subject: [PATCH 31/87] Add section_type to radial distance features --- neurom/features/neurite.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 418b41bf..29c14e7a 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -67,16 +67,17 @@ def _map_sections(fun, neurite, iterator_type=Section.ipreorder, section_type=Ne @feature(shape=()) -def max_radial_distance(neurite): +def max_radial_distance(neurite, section_type=NeuriteType.all): """Get the maximum radial distances of the termination sections.""" - term_radial_distances = section_term_radial_distances(neurite) + term_radial_distances = section_term_radial_distances(neurite, section_type=section_type) return max(term_radial_distances) if term_radial_distances else 0. @feature(shape=()) -def number_of_segments(neurite): +def number_of_segments(neurite, section_type=NeuriteType.all): """Number of segments.""" - return sum(_map_sections(sf.number_of_segments, neurite)) + count_segments = lambda s: len(s.points) - 1 + return sum(_map_sections(count_segments, neurite, section_type=section_type)) @feature(shape=()) @@ -377,7 +378,7 @@ def diameter_power_relations(neurite, method='first'): @feature(shape=(...,)) -def section_radial_distances(neurite, origin=None, iterator_type=Section.ipreorder): +def section_radial_distances(neurite, origin=None, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Section radial distances. The iterator_type can be used to select only terminal sections (ileaf) @@ -386,13 +387,14 @@ def section_radial_distances(neurite, origin=None, iterator_type=Section.ipreord 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) + iterator_type, + section_type=section_type) @feature(shape=(...,)) -def section_term_radial_distances(neurite, origin=None): +def section_term_radial_distances(neurite, origin=None, section_type=NeuriteType.all): """Get the radial distances of the termination sections.""" - return section_radial_distances(neurite, origin, Section.ileaf) + return section_radial_distances(neurite, origin, Section.ileaf, section_type=section_type) @feature(shape=(...,)) From aa9d6e9091ab90186b8901444b1fcec9cc207af4 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 20 Feb 2022 01:27:19 +0100 Subject: [PATCH 32/87] Check feature signature and propagate use_subtrees --- neurom/features/__init__.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 0a4634e7..cc6ed6ec 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -94,23 +94,40 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) 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' + + 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, use_subtrees=use_subtrees, **kwargs) + + import inspect + + if "use_subtrees" in inspect.signature(feature_).parameters: + kwargs["use_subtrees"] = use_subtrees + + 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: From 0e8dc1ceec0ccedded94f26a0b9a82befd858f08 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 20 Feb 2022 11:21:53 +0100 Subject: [PATCH 33/87] Add tests --- tests/test_mixed.py | 95 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index a823889e..d5987a82 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1,5 +1,7 @@ import pytest import neurom +import numpy as np +import numpy.testing as npt from neurom import NeuriteType from neurom.features import get @@ -32,19 +34,86 @@ def mixed_morph(): reader="swc") -def test_morph_number_of_sections_per_neurite(mixed_morph): - - assert get("number_of_sections_per_neurite", mixed_morph, use_subtrees=False) == [3, 5, 3] - assert get("number_of_sections_per_neurite", mixed_morph, use_subtrees=True) == [3, 2, 3, 3] - - assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.basal_dendrite, use_subtrees=False) == [3, 5] - assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.basal_dendrite, use_subtrees=True) == [3, 2] - - assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.axon, use_subtrees=False) == [] - assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.axon, use_subtrees=True) == [3] - - assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.apical_dendrite, use_subtrees=False) == [3] - assert get("number_of_sections_per_neurite", mixed_morph, neurite_type=NeuriteType.apical_dendrite, use_subtrees=True) == [3] +def _morphology_features(): + + features = { + "number_of_sections_per_neurite": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [3, 5, 3], + "expected_with_subtrees": [3, 2, 3, 3], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [3, 5], + "expected_with_subtrees": [3, 2], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [3], + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": [3], + "expected_with_subtrees": [3], + } + ], + "max_radial_distance": [ + { + # without subtrees AoD is considered a single tree, with [3, 3] being the furthest + # with subtrees AoD subtrees are considered separately and the distance is calculated + # from their respective roots. [1, 4] is the furthest point in this case + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": 3.60555127546398, + "expected_with_subtrees": 3.16227766016837, + }, + { + # with a global origin, AoD axon subtree [2, 4] is always furthest from soma + "neurite_type": NeuriteType.all, + "kwargs": {"origin": np.array([0., 0., 0.])}, + "expected_wout_subtrees": 4.47213595499958, + "expected_with_subtrees": 4.47213595499958, + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": 3.60555127546398, # [3, 3] - [0, 1] + "expected_with_subtrees": 3.16227766016837, # [1, 4] - [0, 1] + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"origin": np.array([0., 0., 0.])}, + "expected_wout_subtrees": 4.47213595499958, # [2, 4] - [0, 0] + "expected_with_subtrees": 4.12310562561766, # [1, 4] - [0, 0] + + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 2.23606797749979, # [3, 3] - [1, 2] + }, + { + "neurite_type": NeuriteType.axon, + "kwargs": {"origin": np.array([0., 0., 0.])}, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 4.47213595499958, # [2, 4] - [0, 0] + } + ] + } + + # TODO: Add check here to ensure that there are no features not addressed + + for feature_name, configurations in features.items(): + for cfg in configurations: + kwargs = cfg["kwargs"] if "kwargs" in cfg else {} + yield feature_name, cfg["neurite_type"], kwargs, cfg["expected_wout_subtrees"], cfg["expected_with_subtrees"] + + +@pytest.mark.parametrize("feature_name, neurite_type, kwargs, expected_wout_subtrees, expected_with_subtrees", _morphology_features()) +def test_features__morphology(feature_name, neurite_type, kwargs, expected_wout_subtrees, expected_with_subtrees, mixed_morph): + + npt.assert_allclose(get(feature_name, mixed_morph, neurite_type=neurite_type, use_subtrees=False, **kwargs), expected_wout_subtrees) + npt.assert_allclose(get(feature_name, mixed_morph, neurite_type=neurite_type, use_subtrees=True, **kwargs), expected_with_subtrees) """ def test_mixed__segment_lengths(mixed_morph): From 49cbf8a15d47602d64f7a8fdaf747cf5ce9421be Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 20 Feb 2022 11:22:26 +0100 Subject: [PATCH 34/87] Add origin to morphology max_radial_distance & cleanup --- neurom/features/__init__.py | 4 ++-- neurom/features/morphology.py | 41 ++++++++++++++++++----------------- neurom/features/neurite.py | 10 ++++++--- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index cc6ed6ec..3679a4e8 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -36,6 +36,8 @@ >>> 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 inspect import operator from enum import Enum from functools import reduce @@ -116,8 +118,6 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) feature_ = _MORPHOLOGY_FEATURES[feature_name] - import inspect - if "use_subtrees" in inspect.signature(feature_).parameters: kwargs["use_subtrees"] = use_subtrees diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 1a3bc7fb..563a864c 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -68,13 +68,12 @@ def _homogeneous_subtrees(neurite): """Returns a dictionary the keys of which are section types and the values are the sub-neurites. A sub-neurite can be either the entire tree or a homogeneous downstream sub-tree. - Note: Only two different mixed types are allowed """ homogeneous_neurites = {neurite.root_node.type: neurite} for section in neurite.root_node.ipreorder(): if section.type not in homogeneous_neurites: - homogeneous_neurites[section.type] = Neurite(section) + homogeneous_neurites[section.type] = Neurite(section.morphio_section) if len(homogeneous_neurites) != 2: raise TypeError( f"Subtree types must be exactly two. Found {len(homogeneous_neurites)} instead.") @@ -84,32 +83,32 @@ def _homogeneous_subtrees(neurite): def _map_homogeneous_subtrees(function, neurite, neurite_type): check_type = is_type(neurite_type) - for neurite_type, sub_neurite in _homogeneous_subtrees(neurite).items(): - if check_type(sub_neurite): - yield function(sub_neurite, section_type=neurite_type) + + yield from ( + function(subtree, section_type=section_type) + for section_type, subtree in _homogeneous_subtrees(neurite).items() + if check_type(subtree) + ) -def _map_neurites(function, morph, neurite_type=NeuriteType.all, use_subtrees=False): +def map_neurites(function, obj, neurite_type=NeuriteType.all, use_subtrees=False): """ If `use_subtrees` is enabled, each neurite that is inhomogeneous, is traversed and the subtrees with their respective types are returned. For each of these subtrees the features need to be calculated with a section filter, to ensure no sections from other subtrees are traversed. """ - if use_subtrees: - # we don't know a priori the types of the subtrees in the neurites, thus we cannot - # filter by time at the neurite level yet - for neurite in iter_neurites(morph): + check_type = is_type(neurite_type) - # map the function for each subtree with homogeneous type, propagating the type - # for a section level filtering - if neurite.morphio_root_node.is_heterogeneous(): + if use_subtrees: + for neurite in iter_neurites(obj): + if neurite.is_heterogeneous(): yield from _map_homogeneous_subtrees(function, neurite, neurite_type) else: - if is_type(neurite_type)(neurite): + if check_type(neurite): yield function(neurite) else: - yield from iter_neurites(morph, mapfun=function, filt=is_type(neurite_type)) + yield from iter_neurites(obj, mapfun=function, filt=is_type(neurite_type)) @feature(shape=()) @@ -135,17 +134,19 @@ def soma_radius(morph): @feature(shape=()) -def max_radial_distance(morph, neurite_type=NeuriteType.all): +def max_radial_distance(morph, origin=None, neurite_type=NeuriteType.all, use_subtrees=False): """Get the maximum radial distances of the termination sections.""" - term_radial_distances = _map_neurites(nf.max_radial_distance, morph, neurite_type) - - return max(term_radial_distances) if term_radial_distances else 0.0 + function = partial(nf.max_radial_distance, origin=origin) + term_radial_distances = list( + map_neurites(function, morph, neurite_type, use_subtrees) + ) + return max(term_radial_distances) if term_radial_distances else 0. @feature(shape=(...,)) def number_of_sections_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """List of numbers of sections per neurite.""" - return list(_map_neurites(nf.number_of_sections, morph, neurite_type, use_subtrees)) + return list(map_neurites(nf.number_of_sections, morph, neurite_type, use_subtrees)) @feature(shape=(...,)) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 29c14e7a..e8ce8436 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -67,9 +67,11 @@ def _map_sections(fun, neurite, iterator_type=Section.ipreorder, section_type=Ne @feature(shape=()) -def max_radial_distance(neurite, section_type=NeuriteType.all): +def max_radial_distance(neurite, origin=None, section_type=NeuriteType.all): """Get the maximum radial distances of the termination sections.""" - term_radial_distances = section_term_radial_distances(neurite, section_type=section_type) + term_radial_distances = section_term_radial_distances( + neurite, origin=origin, section_type=section_type + ) return max(term_radial_distances) if term_radial_distances else 0. @@ -378,7 +380,9 @@ def diameter_power_relations(neurite, method='first'): @feature(shape=(...,)) -def section_radial_distances(neurite, origin=None, iterator_type=Section.ipreorder, section_type=NeuriteType.all): +def section_radial_distances( + neurite, origin=None, iterator_type=Section.ipreorder, section_type=NeuriteType.all +): """Section radial distances. The iterator_type can be used to select only terminal sections (ileaf) From 73312ad4a80ec161468d5d411eedc7e980b8e8ce Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 20 Feb 2022 17:23:23 +0100 Subject: [PATCH 35/87] Add morphology total_length_per_neurite --- neurom/features/morphology.py | 6 ++++-- neurom/features/neurite.py | 4 ++-- tests/test_mixed.py | 24 +++++++++++++++++++++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 563a864c..32549594 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -150,9 +150,11 @@ def number_of_sections_per_neurite(morph, neurite_type=NeuriteType.all, use_subt @feature(shape=(...,)) -def total_length_per_neurite(morph, neurite_type=NeuriteType.all): +def total_length_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Neurite lengths.""" - return _map_neurites(nf.total_length, morph, neurite_type) + return list( + map_neurites(nf.total_length, morph, neurite_type, use_subtrees) + ) @feature(shape=(...,)) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index e8ce8436..b1e57ff8 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -107,9 +107,9 @@ def number_of_leaves(neurite): @feature(shape=()) -def total_length(neurite): +def total_length(neurite, section_type=NeuriteType.all): """Neurite length. For a morphology it will be a sum of all neurite lengths.""" - return sum(_map_sections(sf.section_length, neurite)) + return sum(_map_sections(sf.section_length, neurite, section_type=section_type)) @feature(shape=()) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index d5987a82..5fbc88a8 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -18,7 +18,7 @@ def mixed_morph(): 1 1 0 0 0 0.5 -1 2 3 -1 0 0 0.1 1 3 3 -2 0 0 0.1 2 - 4 3 0 -3 0 0.1 3 + 4 3 -3 0 0 0.1 3 5 3 -2 1 0 0.1 3 6 3 0 1 0 0.1 1 7 3 1 2 0 0.1 6 @@ -98,6 +98,28 @@ def _morphology_features(): "expected_wout_subtrees": 0.0, "expected_with_subtrees": 4.47213595499958, # [2, 4] - [0, 0] } + ], + "total_length_per_neurite": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [3., 6.82842712474619, 3.], + "expected_with_subtrees": [3., 3.414213562373095, 3.414213562373095, 3], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [3., 6.82842712474619], + "expected_with_subtrees": [3., 3.414213562373095], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [3.414213562373095], + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": [3.], + "expected_with_subtrees": [3.], + } ] } From 8d1d172ef5738079c8a3c33d43bf57c41cae15b4 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 20 Feb 2022 18:04:31 +0100 Subject: [PATCH 36/87] Add total_area_per_neurite morphology feature --- neurom/core/morphology.py | 6 ++++-- neurom/features/morphology.py | 6 ++++-- neurom/features/neurite.py | 4 ++-- tests/test_mixed.py | 26 ++++++++++++++++++++++++-- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 6ba333e7..52193671 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -368,7 +368,8 @@ def length(self): The length is defined as the sum of lengths of the sections. """ - return sum(s.length for s in self.iter_sections()) + from neurom.features.neurite import total_length + return total_length(self) @property def area(self): @@ -376,7 +377,8 @@ def area(self): The area is defined as the sum of area of the sections. """ - return sum(s.area for s in self.iter_sections()) + from neurom.features.neurite import total_area + return total_area(self) @property def volume(self): diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 32549594..d6a4253b 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -158,9 +158,11 @@ def total_length_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=F @feature(shape=(...,)) -def total_area_per_neurite(morph, neurite_type=NeuriteType.all): +def total_area_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Neurite areas.""" - return _map_neurites(nf.total_area, morph, neurite_type) + return list( + map_neurites(nf.total_area, morph, neurite_type, use_subtrees) + ) @feature(shape=(...,)) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index b1e57ff8..7cfcae07 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -113,12 +113,12 @@ def total_length(neurite, section_type=NeuriteType.all): @feature(shape=()) -def total_area(neurite): +def total_area(neurite, section_type=NeuriteType.all): """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 sum(_map_sections(sf.section_area, neurite)) + return sum(_map_sections(sf.section_area, neurite, section_type=section_type)) @feature(shape=()) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 5fbc88a8..c3a77a9e 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -120,6 +120,28 @@ def _morphology_features(): "expected_wout_subtrees": [3.], "expected_with_subtrees": [3.], } + ], + "total_area_per_neurite" : [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [1.884956, 4.290427, 1.884956], # total_length * 2piR + "expected_with_subtrees": [1.884956, 2.145214, 2.145214, 1.884956], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [1.884956, 4.290427], + "expected_with_subtrees": [1.884956, 2.145214], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [2.145214], + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": [1.884956], + "expected_with_subtrees": [1.884956], + } ] } @@ -134,8 +156,8 @@ def _morphology_features(): @pytest.mark.parametrize("feature_name, neurite_type, kwargs, expected_wout_subtrees, expected_with_subtrees", _morphology_features()) def test_features__morphology(feature_name, neurite_type, kwargs, expected_wout_subtrees, expected_with_subtrees, mixed_morph): - npt.assert_allclose(get(feature_name, mixed_morph, neurite_type=neurite_type, use_subtrees=False, **kwargs), expected_wout_subtrees) - npt.assert_allclose(get(feature_name, mixed_morph, neurite_type=neurite_type, use_subtrees=True, **kwargs), expected_with_subtrees) + npt.assert_allclose(get(feature_name, mixed_morph, neurite_type=neurite_type, use_subtrees=False, **kwargs), expected_wout_subtrees, rtol=1e-6) + npt.assert_allclose(get(feature_name, mixed_morph, neurite_type=neurite_type, use_subtrees=True, **kwargs), expected_with_subtrees, rtol=1e-6) """ def test_mixed__segment_lengths(mixed_morph): From ebcdf0233e1d895c8d852724359850321b9cdaa8 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 20 Feb 2022 19:50:58 +0100 Subject: [PATCH 37/87] Add soma features to tests --- tests/test_mixed.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index c3a77a9e..c9eaeec0 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -37,6 +37,27 @@ def mixed_morph(): def _morphology_features(): features = { + "soma_radius": [ + { + "neurite_type": None, + "expected_wout_subtrees": 0.5, + "expected_with_subtrees": 0.5, + } + ], + "soma_surface_area": [ + { + "neurite_type": None, + "expected_wout_subtrees": np.pi, + "expected_with_subtrees": np.pi, + } + ], + "soma_volume": [ + { + "neurite_type": None, + "expected_wout_subtrees": np.pi / 6., + "expected_with_subtrees": np.pi / 6., + } + ], "number_of_sections_per_neurite": [ { "neurite_type": NeuriteType.all, @@ -156,8 +177,23 @@ def _morphology_features(): @pytest.mark.parametrize("feature_name, neurite_type, kwargs, expected_wout_subtrees, expected_with_subtrees", _morphology_features()) def test_features__morphology(feature_name, neurite_type, kwargs, expected_wout_subtrees, expected_with_subtrees, mixed_morph): - npt.assert_allclose(get(feature_name, mixed_morph, neurite_type=neurite_type, use_subtrees=False, **kwargs), expected_wout_subtrees, rtol=1e-6) - npt.assert_allclose(get(feature_name, mixed_morph, neurite_type=neurite_type, use_subtrees=True, **kwargs), expected_with_subtrees, rtol=1e-6) + kwargs["use_subtrees"] = False + + if neurite_type is not None: + kwargs["neurite_type"] = neurite_type + + npt.assert_allclose( + get(feature_name, mixed_morph, **kwargs), + expected_wout_subtrees, + rtol=1e-6 + ) + + kwargs["use_subtrees"] = True + npt.assert_allclose( + get(feature_name, mixed_morph, **kwargs), + expected_with_subtrees, + rtol=1e-6 + ) """ def test_mixed__segment_lengths(mixed_morph): From a15a511846cbcd1bd65f897ac971afa24315c0de Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 21 Feb 2022 10:32:48 +0100 Subject: [PATCH 38/87] Convert total_volume_per_neurite feature --- neurom/core/morphology.py | 6 +++++- neurom/features/morphology.py | 6 ++++-- neurom/features/neurite.py | 10 ++++++++-- tests/test_mixed.py | 22 ++++++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 52193671..d1df2b30 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -368,6 +368,7 @@ def length(self): The length is defined as the sum of lengths of the sections. """ + # pylint: disable=import-outside-toplevel from neurom.features.neurite import total_length return total_length(self) @@ -377,6 +378,7 @@ def area(self): The area is defined as the sum of area of the sections. """ + # pylint: disable=import-outside-toplevel from neurom.features.neurite import total_area return total_area(self) @@ -386,7 +388,9 @@ def volume(self): The volume is defined as the sum of volumes of the sections. """ - return sum(s.volume for s in self.iter_sections()) + # pylint: disable=import-outside-toplevel + from neurom.features.neurite import total_volume + return total_volume(self) def is_heterogeneous(self) -> bool: """Returns true if the neurite consists of more that one section types""" diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index d6a4253b..8204ddfb 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -166,9 +166,11 @@ def total_area_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=Fal @feature(shape=(...,)) -def total_volume_per_neurite(morph, neurite_type=NeuriteType.all): +def total_volume_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Neurite volumes.""" - return _map_neurites(nf.total_volume, morph, neurite_type) + return list( + map_neurites(nf.total_volume, morph, neurite_type, use_subtrees) + ) @feature(shape=(...,)) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 7cfcae07..e064570c 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -122,9 +122,15 @@ def total_area(neurite, section_type=NeuriteType.all): @feature(shape=()) -def total_volume(neurite): +def total_volume(neurite, section_type=NeuriteType.all): """Neurite volume. For a morphology it will be a sum of neurites volumes.""" - return sum(_map_sections(sf.section_volume, neurite)) + return sum(_map_sections(sf.section_volume, neurite, section_type=section_type)) + + +def _section_length(section): + """Get section length of `section`.""" + return morphmath.section_length(section.points) +>>>>>>> Convert total_volume_per_neurite feature @feature(shape=(...,)) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index c9eaeec0..1b4baf05 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -163,6 +163,28 @@ def _morphology_features(): "expected_wout_subtrees": [1.884956], "expected_with_subtrees": [1.884956], } + ], + "total_volume_per_neurite": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [0.09424778, 0.21452136, 0.09424778], # total_length * piR^2 + "expected_with_subtrees": [0.09424778, 0.10726068, 0.10726068, 0.09424778], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [0.09424778, 0.21452136], + "expected_with_subtrees": [0.09424778, 0.10726068], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.10726068], + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": [0.09424778], + "expected_with_subtrees": [0.09424778], + } ] } From 188bc910c762ee4231b6777b588e3b6a1c1e93d3 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 21 Feb 2022 13:42:19 +0100 Subject: [PATCH 39/87] Trunk origin azimuths, elevations & angles are not valid for distal subtrees --- tests/test_mixed.py | 74 +++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 1b4baf05..891408a9 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -185,7 +185,58 @@ def _morphology_features(): "expected_wout_subtrees": [0.09424778], "expected_with_subtrees": [0.09424778], } - ] + ], + "trunk_origin_azimuths": [ # Not applicable to distal subtrees + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [3.1415927, 0.0, 0.0], + "expected_with_subtrees": [3.1415927, 0.0, 0.0], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [3.1415927, 0.0], + "expected_with_subtrees": [3.1415927, 0.0], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [], + }, + ], + "trunk_origin_elevations": [ # Not applicable to distal subtrees + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [0.0, 1.5707964, -1.5707964], + "expected_with_subtrees": [0.0, 1.5707964, -1.5707964], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [0.0, 1.5707964], + "expected_with_subtrees": [0.0, 1.5707964], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [], + }, + ], + "trunk_angles": [ # Not applicable to distal subtrees + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [1.570796, 3.141592, 1.570796], + "expected_with_subtrees": [1.570796, 3.141592, 1.570796], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [1.5707964, 1.570796], + "expected_with_subtrees": [1.5707964, 1.570796], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [], + }, + ], } # TODO: Add check here to ensure that there are no features not addressed @@ -218,27 +269,6 @@ def test_features__morphology(feature_name, neurite_type, kwargs, expected_wout_ ) """ -def test_mixed__segment_lengths(mixed_morph): - - axon_on_dendrite = mixed_morph.neurites[0] - - get("segment_lengths", process_inhomogeneous_subtrees=True, neurite_type=NeuriteType.axon) - -def test_features(mixed_morph): - - # the traditional way processes each tree as a whole - assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=False) == 1 - assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=False, neurite_type=NeuriteType.basal_dendrite) == 1 - assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=False, neurite_type=NeuriteType.axon) == 0 - - # the new way checks for inhomogeneous subtrees anc counts them as separate neurites - assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=True) == 2 - assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=True, neurite_type=NeuriteType.basal_dendrite) == 1 - assert get("number_of_neurites", mixed_morph, process_inhomogeneous_subtrees=True, neurite_type=NeuriteType.axon) == 1 - - - - def test_mixed_types(mixed_morph): from neurom import NeuriteType From f3d589a7eb143313ab09e0ba4eca71aba310d8bd Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 24 Feb 2022 14:51:11 +0100 Subject: [PATCH 40/87] Add section_filter in iter_sections --- neurom/core/morphology.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index d1df2b30..b59edbbf 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -256,7 +256,8 @@ def iter_neurites(obj, mapfun=None, filt=None, neurite_order=NeuriteIter.FileOrd def iter_sections(neurites, iterator_type=Section.ipreorder, neurite_filter=None, - neurite_order=NeuriteIter.FileOrder): + neurite_order=NeuriteIter.FileOrder, + section_filter=None): """Iterator to the sections in a neurite, morphology or morphology population. Arguments: @@ -282,11 +283,13 @@ def iter_sections(neurites, >>> filter = lambda n : n.type == nm.AXON >>> n_points = [len(s.points) for s in iter_sections(pop, neurite_filter=filter)] """ - return flatten( - iterator_type(neurite.root_node) - for neurite in iter_neurites(neurites, filt=neurite_filter, neurite_order=neurite_order) + sections = flatten( + iterator_type(neurite.root_node) for neurite in + iter_neurites(neurites, filt=neurite_filter, neurite_order=neurite_order) ) + return sections if section_filter is None else filter(section_filter, sections) + def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder): """Return an iterator to the segments in a collection of neurites. From 31d93321fa16e1ba590c56d6499f99ae76e2df01 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 24 Feb 2022 14:51:38 +0100 Subject: [PATCH 41/87] Convert more features --- neurom/features/morphology.py | 97 ++++++++++++++------- tests/test_mixed.py | 155 ++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 32 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 8204ddfb..2c18c97c 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -45,12 +45,13 @@ import warnings +from itertools import chain 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.morphology import iter_neurites, iter_sections, 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 @@ -80,15 +81,28 @@ def _homogeneous_subtrees(neurite): return homogeneous_neurites +def _iter_subneurites(obj, neurite_type=NeuriteType.all, use_subtrees=False): + + def extract_subneurites(neurite): + if neurite.is_heterogeneous(): + return [subtree for _, subtree in _homogeneous_subtrees(neurite).items()] + return [neurite] + + neurites = iter_neurites(obj) + + if use_subtrees: + neurites = chain.from_iterable(map(extract_subneurites, neurites)) + + yield from filter(is_type(neurite_type), neurites) + + def _map_homogeneous_subtrees(function, neurite, neurite_type): check_type = is_type(neurite_type) - yield from ( - function(subtree, section_type=section_type) - for section_type, subtree in _homogeneous_subtrees(neurite).items() - if check_type(subtree) - ) + for section_type, subtree in _homogeneous_subtrees(neurite).items(): + if check_type(subtree): + yield function(subtree, section_type=section_type) def map_neurites(function, obj, neurite_type=NeuriteType.all, use_subtrees=False): @@ -211,12 +225,12 @@ def elevation(neurite): @feature(shape=(...,)) -def trunk_vectors(morph, neurite_type=NeuriteType.all): +def trunk_vectors(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Calculate the vectors between all the trunks of the morphology and the soma center.""" def vector_to_root_node(neurite): return morphmath.vector(neurite.root_node.points[0], morph.soma.center) - return _map_neurites(vector_to_root_node, morph, neurite_type) + return _map_neurites(vector_to_root_node, morph, neurite_type, use_subtrees) @feature(shape=(...,)) @@ -407,6 +421,7 @@ def trunk_origin_radii( neurite_type=NeuriteType.all, min_length_filter=None, max_length_filter=None, + use_subtrees=False, ): """Radii of the trunk sections of neurites in a morph. @@ -429,9 +444,8 @@ def trunk_origin_radii( * else the mean radius of the points between the given ``min_length_filter`` and ``max_length_filter`` are returned. """ - if max_length_filter is None and min_length_filter is None: - return [n.root_node.points[0][COLS.R] - for n in iter_neurites(morph, filt=is_type(neurite_type))] + def trunk_first_radius(neurite): + return neurite.root_node.points[0][COLS.R] if min_length_filter is not None and min_length_filter <= 0: raise NeuroMError( @@ -455,11 +469,14 @@ def trunk_origin_radii( "'max_length_filter' value." ) - def _mean_radius(neurite): + def trunk_mean_radius(neurite): + points = neurite.root_node.points + interval_lengths = morphmath.interval_lengths(points) path_lengths = np.insert(np.cumsum(interval_lengths), 0, 0) valid_pts = np.ones(len(path_lengths), dtype=bool) + if min_length_filter is not None: valid_pts = (valid_pts & (path_lengths >= min_length_filter)) if not valid_pts.any(): @@ -469,6 +486,7 @@ def _mean_radius(neurite): "point is returned." ) return points[-1, COLS.R] + if max_length_filter is not None: valid_max = (path_lengths <= max_length_filter) valid_pts = (valid_pts & valid_max) @@ -478,26 +496,33 @@ def _mean_radius(neurite): "values excluded all the points of the section so the radius of the first " "point after the 'min_length_filter' path distance is returned." ) - # pylint: disable=invalid-unary-operand-type return points[~valid_max, COLS.R][0] + return points[valid_pts, COLS.R].mean() - return _map_neurites(_mean_radius, morph, neurite_type) + function = ( + trunk_first_radius + if max_length_filter is None and min_length_filter is None + else trunk_mean_radius + ) + + return map_neurites(function, morph, neurite_type, use_subtrees) @feature(shape=(...,)) -def trunk_section_lengths(morph, neurite_type=NeuriteType.all): +def trunk_section_lengths(morph, neurite_type=NeuriteType.all, use_subtrees=False): """List of lengths of trunk sections of neurites in a morph.""" def trunk_section_length(neurite): return morphmath.section_length(neurite.root_node.points) - return _map_neurites(trunk_section_length, morph, neurite_type) + return [morphmath.section_length(n.root_node.points) + for n in _iter_subneurites(morph, neurite_type, use_subtrees)] @feature(shape=()) -def number_of_neurites(morph, neurite_type=NeuriteType.all): +def number_of_neurites(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Number of neurites in a morph.""" - return len(_map_neurites(lambda n: n, morph, neurite_type)) + return len(map_neurites(lambda n: n, morph, neurite_type, use_subtrees)) @feature(shape=(...,)) @@ -593,38 +618,46 @@ def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None return sholl_crossings(morph, neurite_type, morph.soma.center, bins) -def _extent_along_axis(morph, axis, neurite_type): +def _extent_along_axis(morph, axis, neurite_type, use_subtrees=False): """Returns the total extent of the morpholog neurites. The morphology is filtered by neurite type and the extent is calculated along the coordinate axis direction (e.g. COLS.X). """ - it_points = ( - p - for n in iter_neurites(morph, filt=is_type(neurite_type)) - for p in n.points[:, axis] + def iter_coordinates(neurite, section_type=NeuriteType.all): + return ( + coordinate + for section in iter_sections(neurite, section_filter=is_type(section_type)) + for coordinate in section.points[:, axis] + ) + + axis_coordinates = np.fromiter( + chain.from_iterable( + map_neurites(iter_coordinates, morph, neurite_type, use_subtrees) + ), + dtype=np.float32 ) - try: - return abs(np.ptp(np.fromiter(it_points, dtype=np.float32))) - except ValueError: - # a ValueError is thrown when there are no points passed to ptp + + if len(axis_coordinates) == 0: return 0.0 + return abs(np.ptp(axis_coordinates)) + @feature(shape=()) -def total_width(morph, neurite_type=NeuriteType.all): +def total_width(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Extent of morphology along axis x.""" - return _extent_along_axis(morph, axis=COLS.X, neurite_type=neurite_type) + return _extent_along_axis(morph, COLS.X, neurite_type, use_subtrees) @feature(shape=()) -def total_height(morph, neurite_type=NeuriteType.all): +def total_height(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Extent of morphology along axis y.""" - return _extent_along_axis(morph, axis=COLS.Y, neurite_type=neurite_type) + return _extent_along_axis(morph, COLS.Y, neurite_type, use_subtrees) @feature(shape=()) -def total_depth(morph, neurite_type=NeuriteType.all): +def total_depth(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Extent of morphology along axis z.""" return _extent_along_axis(morph, axis=COLS.Z, neurite_type=neurite_type) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 891408a9..c1552729 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -220,6 +220,29 @@ def _morphology_features(): "expected_with_subtrees": [], }, ], + "trunk_vectors": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [[-1., 0., 0.], [0., 1., 0.], [0., -1., 0.]], + "expected_with_subtrees": [[-1., 0., 0.], [0., 1., 0.], [1., 2., 0.], [0., -1., 0.]], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [[-1., 0., 0.], [0., 1., 0.]], + "expected_with_subtrees": [[-1., 0., 0.], [0., 1., 0.]], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [[1., 2., 0.]], + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": [[0., -1., 0.]], + "expected_with_subtrees": [[0., -1., 0.]], + }, + + ], "trunk_angles": [ # Not applicable to distal subtrees { "neurite_type": NeuriteType.all, @@ -237,6 +260,138 @@ def _morphology_features(): "expected_with_subtrees": [], }, ], + "trunk_origin_radii": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [0.1, 0.1, 0.1], + "expected_with_subtrees": [0.1, 0.1, 0.1, 0.1], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [0.1, 0.1], + "expected_with_subtrees": [0.1, 0.1], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.1], + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": [0.1], + "expected_with_subtrees": [0.1], + }, + ], + "trunk_section_lengths": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [1., 1.414213, 1.], + "expected_with_subtrees": [1., 1.414213, 1.414213, 1.], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [1., 1.414213], + "expected_with_subtrees": [1., 1.414213], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.414213], + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": [1.], + "expected_with_subtrees": [1.], + }, + ], + "number_of_neurites": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": 3, + "expected_with_subtrees": 4, + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": 2, + "expected_with_subtrees": 2, + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": 1, + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": 1, + "expected_with_subtrees": 1, + }, + ], + "total_width": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": 6.0, + "expected_with_subtrees": 6.0, + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": 6.0, + "expected_with_subtrees": 4.0, + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 2.0, + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": 1.0, + "expected_with_subtrees": 1.0, + }, + ], + "total_height": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": 7.0, + "expected_with_subtrees": 7.0, + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": 4.0, + "expected_with_subtrees": 4.0, + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 2.0, + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": 2.0, + "expected_with_subtrees": 2.0, + }, + ], + "total_depth": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 0.0, + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 0.0, + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 0.0, + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 0.0, + }, + ], } # TODO: Add check here to ensure that there are no features not addressed From ff21783d7aa0a5a30d3087469f7f5c42042c16f9 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 24 Feb 2022 19:53:09 +0100 Subject: [PATCH 42/87] Add section filter in iter_segments --- neurom/core/morphology.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index b59edbbf..d499d346 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -291,7 +291,7 @@ def iter_sections(neurites, return sections if section_filter is None else filter(section_filter, sections) -def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder): +def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder, section_filter=None): """Return an iterator to the segments in a collection of neurites. Arguments: @@ -309,7 +309,8 @@ def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder) sections = iter((obj,) if isinstance(obj, Section) else iter_sections(obj, neurite_filter=neurite_filter, - neurite_order=neurite_order)) + neurite_order=neurite_order, + section_filter=section_filter)) return flatten( zip(section.points[:-1], section.points[1:]) From 926ddf1762882d82ea1be6b01a952038f9e0dbec Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 24 Feb 2022 19:53:45 +0100 Subject: [PATCH 43/87] More features --- neurom/core/morphology.py | 4 +- neurom/features/morphology.py | 59 +++++++++++++------ tests/test_mixed.py | 103 +++++++++++++++++++++++++++++++++- 3 files changed, 145 insertions(+), 21 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index d499d346..f4bf56ff 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -291,7 +291,9 @@ def iter_sections(neurites, return sections if section_filter is None else filter(section_filter, sections) -def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder, section_filter=None): +def iter_segments( + obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder, section_filter=None +): """Return an iterator to the segments in a collection of neurites. Arguments: diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 2c18c97c..9292c5da 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -311,6 +311,7 @@ def trunk_angles_inter_types( source_neurite_type=NeuriteType.apical_dendrite, target_neurite_type=NeuriteType.basal_dendrite, closest_component=None, + use_subtrees=False, ): """Calculate the angles between the trunks of the morph of a source type to target type. @@ -338,8 +339,12 @@ def trunk_angles_inter_types( If ``closest_component`` is not ``None``, only one of these values is returned for each couple. """ - source_vectors = trunk_vectors(morph, neurite_type=source_neurite_type) - target_vectors = trunk_vectors(morph, neurite_type=target_neurite_type) + source_vectors = trunk_vectors( + morph, neurite_type=source_neurite_type, use_subtrees=use_subtrees + ) + target_vectors = trunk_vectors( + morph, neurite_type=target_neurite_type, use_subtrees=use_subtrees + ) # In order to avoid the failure of the process in case the neurite_type does not exist if len(source_vectors) == 0 or len(target_vectors) == 0: @@ -374,6 +379,7 @@ def trunk_angles_from_vector( morph, neurite_type=NeuriteType.all, vector=None, + use_subtrees=False, ): """Calculate the angles between the trunks of the morph of a given type and a given vector. @@ -393,7 +399,7 @@ def trunk_angles_from_vector( if vector is None: vector = (0, 1, 0) - vectors = np.array(trunk_vectors(morph, neurite_type=neurite_type)) + vectors = np.array(trunk_vectors(morph, neurite_type=neurite_type, use_subtrees=use_subtrees)) # In order to avoid the failure of the process in case the neurite_type does not exist if len(vectors) == 0: @@ -532,7 +538,9 @@ def neurite_volume_density(morph, neurite_type=NeuriteType.all): @feature(shape=(...,)) -def sholl_crossings(morph, neurite_type=NeuriteType.all, center=None, radii=None): +def sholl_crossings( + morph, neurite_type=NeuriteType.all, center=None, radii=None, use_subtrees=False +): """Calculate crossings of neurites. Args: @@ -553,11 +561,11 @@ def sholl_crossings(morph, neurite_type=NeuriteType.all, center=None, radii=None center=morph.soma.center, radii=np.arange(0, 1000, 100)) """ - def _count_crossings(neurite, radius): + def count_crossings(section, radius): """Used to count_crossings of segments in neurite with radius.""" r2 = radius ** 2 count = 0 - for start, end in iter_segments(neurite): + for start, end in iter_segments(section): start_dist2, end_dist2 = (morphmath.point_dist2(center, start), morphmath.point_dist2(center, end)) @@ -574,13 +582,25 @@ def _count_crossings(neurite, radius): 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] + + if use_subtrees: + sections = iter_sections(morph, section_filter=is_type(neurite_type)) + else: + sections = iter_sections(morph, neurite_filter=is_type(neurite_type)) + + counts_per_radius = [0 for _ in range(len(radii))] + + for section in sections: + for i, radius in enumerate(radii): + counts_per_radius[i] += count_crossings(section, radius) + + return counts_per_radius @feature(shape=(...,)) -def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None): +def sholl_frequency( + morph, neurite_type=NeuriteType.all, step_size=10, bins=None, use_subtrees=False +): """Perform Sholl frequency calculations on a morph. Args: @@ -600,22 +620,25 @@ def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None If a `neurite_type` is specified and there are no trees corresponding to it, an empty list will be returned. """ - neurite_filter = is_type(neurite_type) - if bins is None: min_soma_edge = morph.soma.radius - max_radius_per_neurite = [ - np.max(np.linalg.norm(n.points[:, COLS.XYZ] - morph.soma.center, axis=1)) - for n in morph.neurites if neurite_filter(n) + if use_subtrees: + sections = iter_sections(morph, section_filter=is_type(neurite_type)) + else: + sections = iter_sections(morph, neurite_filter=is_type(neurite_type)) + + max_radius_per_section = [ + np.max(np.linalg.norm(section.points[:, COLS.XYZ] - morph.soma.center, axis=1)) + for section in sections ] - if not max_radius_per_neurite: + if not max_radius_per_section: return [] - bins = np.arange(min_soma_edge, min_soma_edge + max(max_radius_per_neurite), step_size) + bins = np.arange(min_soma_edge, min_soma_edge + max(max_radius_per_section), step_size) - return sholl_crossings(morph, neurite_type, morph.soma.center, bins) + return sholl_crossings(morph, neurite_type, morph.soma.center, bins, use_subtrees=use_subtrees) def _extent_along_axis(morph, axis, neurite_type, use_subtrees=False): diff --git a/tests/test_mixed.py b/tests/test_mixed.py index c1552729..aa1ab278 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -4,7 +4,7 @@ import numpy.testing as npt from neurom import NeuriteType from neurom.features import get - +from neurom.features import _MORPHOLOGY_FEATURES @pytest.fixture def mixed_morph(): @@ -260,6 +260,47 @@ def _morphology_features(): "expected_with_subtrees": [], }, ], + "trunk_angles_from_vector": [ + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [ + [np.pi / 2., - np.pi / 2, np.pi], + [0., 0., 0.], + [np.pi, np.pi, 0.], + ], + "expected_with_subtrees": [ + [np.pi / 2., - np.pi / 2, np.pi], + [0., 0., 0.], + [0.463648, -0.463648, 0.], + [np.pi, np.pi, 0.], + ], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [[np.pi / 2., - np.pi / 2, np.pi], [0., 0., 0.]], + "expected_with_subtrees": [[np.pi / 2., - np.pi / 2, np.pi], [0., 0., 0.]], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [[0.463648, -0.463648, 0.]], + }, + + ], + "trunk_angles_inter_types": [ + { + "neurite_type": None, + "kwargs": { + "source_neurite_type": NeuriteType.basal_dendrite, + "target_neurite_type": NeuriteType.axon, + }, + "expected_wout_subtrees": [], + "expected_with_subtrees": [ + [[ 2.034444, 1.107149, -3.141593]], + [[ 0.463648, -0.463648, 0. ]], + ], + }, + ], "trunk_origin_radii": [ { "neurite_type": NeuriteType.all, @@ -326,6 +367,59 @@ def _morphology_features(): "expected_with_subtrees": 1, }, ], + "sholl_crossings": [ + { + "neurite_type": NeuriteType.all, + "kwargs": {"radii": [1.5, 3.5]}, + "expected_wout_subtrees": [3, 2], + "expected_with_subtrees": [3, 2], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"radii": [1.5, 3.5]}, + "expected_wout_subtrees": [2, 2], + "expected_with_subtrees": [2, 1], + }, + { + "neurite_type": NeuriteType.axon, + "kwargs": {"radii": [1.5, 3.5]}, + "expected_wout_subtrees": [0, 0], + "expected_with_subtrees": [0, 1], + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"radii": [1.5, 3.5]}, + "expected_wout_subtrees": [1, 0], + "expected_with_subtrees": [1, 0], + }, + ], + "sholl_frequency": [ + { + "neurite_type": NeuriteType.all, + "kwargs": {"step_size": 3}, + "expected_wout_subtrees": [0, 2], + "expected_with_subtrees": [0, 2], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"step_size": 3}, + "expected_wout_subtrees": [0, 2], + "expected_with_subtrees": [0, 1], + }, +# { see #987 +# "neurite_type": NeuriteType.axon, +# "kwargs": {"step_size": 3}, +# "expected_wout_subtrees": [0, 0], +# "expected_with_subtrees": [0, 1], +# }, + { + "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"step_size": 2}, + "expected_wout_subtrees": [0, 1], + "expected_with_subtrees": [0, 1], + }, + + ], "total_width": [ { "neurite_type": NeuriteType.all, @@ -394,7 +488,12 @@ def _morphology_features(): ], } - # TODO: Add check here to ensure that there are no features not addressed + #features_not_tested = set(_MORPHOLOGY_FEATURES) - set(features.keys()) + + #assert not features_not_tested, ( + # "The following morphology tests need to be included in the mixed morphology tests:\n" + # f"{features_not_tested}" + #) for feature_name, configurations in features.items(): for cfg in configurations: From 11fa60401816deb884e7a405d663bb07fbd2c93c Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 24 Feb 2022 23:55:30 +0100 Subject: [PATCH 44/87] Add neurite volume density --- neurom/features/morphology.py | 14 ++++++--- neurom/features/neurite.py | 29 +++++++++++++++--- tests/test_mixed.py | 58 +++++++++++++++++++++++++---------- 3 files changed, 76 insertions(+), 25 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 9292c5da..064c88a5 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -47,6 +47,7 @@ from itertools import chain from functools import partial +from collections.abc import Iterable import math import numpy as np @@ -532,9 +533,9 @@ def number_of_neurites(morph, neurite_type=NeuriteType.all, use_subtrees=False): @feature(shape=(...,)) -def neurite_volume_density(morph, neurite_type=NeuriteType.all): +def neurite_volume_density(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Get volume density per neurite.""" - return _map_neurites(nf.volume_density, morph, neurite_type) + return list(map_neurites(nf.volume_density, morph, neurite_type, use_subtrees)) @feature(shape=(...,)) @@ -583,10 +584,13 @@ def count_crossings(section, radius): if radii is None: radii = [morph.soma.radius] - if use_subtrees: - sections = iter_sections(morph, section_filter=is_type(neurite_type)) + if isinstance(morph, Iterable): + sections = filter(is_type(neurite_type), morph) else: - sections = iter_sections(morph, neurite_filter=is_type(neurite_type)) + if use_subtrees: + sections = iter_sections(morph, section_filter=is_type(neurite_type)) + else: + sections = iter_sections(morph, neurite_filter=is_type(neurite_type)) counts_per_radius = [0 for _ in range(len(radii))] diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index e064570c..4d074289 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -49,7 +49,7 @@ import numpy as np from neurom import morphmath from neurom.core.types import NeuriteType -from neurom.core.morphology import Section +from neurom.core.morphology import Section, iter_sections from neurom.core.dataformat import COLS from neurom.features import NameSpace, feature, bifurcation as bf, section as sf from neurom.morphmath import convex_hull @@ -420,7 +420,7 @@ def terminal_path_lengths(neurite): @feature(shape=()) -def volume_density(neurite): +def volume_density(neurite, section_type=NeuriteType.all): """Get the volume density. The volume density is defined as the ratio of the neurite volume and @@ -431,8 +431,29 @@ def volume_density(neurite): .. note:: Returns `np.nan` if the convex hull computation fails. """ - neurite_hull = convex_hull(neurite.points[:, COLS.XYZ]) - return neurite.volume / neurite_hull.volume if neurite_hull is not None else np.nan + try: + + if section_type != NeuriteType.all: + + sections = list(iter_sections(neurite, section_filter=is_type(section_type))) + points = [ + point + for section in sections + for point in section.points[:, COLS.XYZ] + ] + volume = convex_hull(points).volume + neurite_volume = sum(s.volume for s in sections) + + else: + volume = convex_hull(neurite).volume + neurite_volume = 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=(...,)) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index aa1ab278..07b23402 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1,3 +1,4 @@ +import warnings import pytest import neurom import numpy as np @@ -367,6 +368,28 @@ def _morphology_features(): "expected_with_subtrees": 1, }, ], + "neurite_volume_density": [ # our morphology is flat :( + { + "neurite_type": NeuriteType.all, + "expected_wout_subtrees": [np.nan, np.nan, np.nan], + "expected_with_subtrees": [np.nan, np.nan, np.nan, np.nan], + }, + { + "neurite_type": NeuriteType.basal_dendrite, + "expected_wout_subtrees": [np.nan, np.nan], + "expected_with_subtrees": [np.nan, np.nan], + }, + { + "neurite_type": NeuriteType.axon, + "expected_wout_subtrees": [], + "expected_with_subtrees": [np.nan], + }, + { + "neurite_type": NeuriteType.apical_dendrite, + "expected_wout_subtrees": [np.nan], + "expected_with_subtrees": [np.nan], + }, + ], "sholl_crossings": [ { "neurite_type": NeuriteType.all, @@ -488,12 +511,12 @@ def _morphology_features(): ], } - #features_not_tested = set(_MORPHOLOGY_FEATURES) - set(features.keys()) + features_not_tested = set(_MORPHOLOGY_FEATURES) - set(features.keys()) - #assert not features_not_tested, ( - # "The following morphology tests need to be included in the mixed morphology tests:\n" - # f"{features_not_tested}" - #) + assert not features_not_tested, ( + "The following morphology tests need to be included in the mixed morphology tests:\n" + f"{features_not_tested}" + ) for feature_name, configurations in features.items(): for cfg in configurations: @@ -509,18 +532,21 @@ def test_features__morphology(feature_name, neurite_type, kwargs, expected_wout_ if neurite_type is not None: kwargs["neurite_type"] = neurite_type - npt.assert_allclose( - get(feature_name, mixed_morph, **kwargs), - expected_wout_subtrees, - rtol=1e-6 - ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") - kwargs["use_subtrees"] = True - npt.assert_allclose( - get(feature_name, mixed_morph, **kwargs), - expected_with_subtrees, - rtol=1e-6 - ) + npt.assert_allclose( + get(feature_name, mixed_morph, **kwargs), + expected_wout_subtrees, + rtol=1e-6 + ) + + kwargs["use_subtrees"] = True + npt.assert_allclose( + get(feature_name, mixed_morph, **kwargs), + expected_with_subtrees, + rtol=1e-6 + ) """ def test_mixed_types(mixed_morph): From aaaf4d1d30c19132c93f20825c0d16cdd581eed8 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 25 Feb 2022 00:35:31 +0100 Subject: [PATCH 45/87] Use kwargs only in tests --- tests/test_mixed.py | 172 ++++++++++++++++++++------------------------ 1 file changed, 78 insertions(+), 94 deletions(-) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 07b23402..bb67fce6 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -40,43 +40,40 @@ def _morphology_features(): features = { "soma_radius": [ { - "neurite_type": None, "expected_wout_subtrees": 0.5, "expected_with_subtrees": 0.5, } ], "soma_surface_area": [ { - "neurite_type": None, "expected_wout_subtrees": np.pi, "expected_with_subtrees": np.pi, } ], "soma_volume": [ { - "neurite_type": None, "expected_wout_subtrees": np.pi / 6., "expected_with_subtrees": np.pi / 6., } ], "number_of_sections_per_neurite": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [3, 5, 3], "expected_with_subtrees": [3, 2, 3, 3], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [3, 5], "expected_with_subtrees": [3, 2], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [3], }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": [3], "expected_with_subtrees": [3], } @@ -86,159 +83,156 @@ def _morphology_features(): # without subtrees AoD is considered a single tree, with [3, 3] being the furthest # with subtrees AoD subtrees are considered separately and the distance is calculated # from their respective roots. [1, 4] is the furthest point in this case - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": 3.60555127546398, "expected_with_subtrees": 3.16227766016837, }, { # with a global origin, AoD axon subtree [2, 4] is always furthest from soma - "neurite_type": NeuriteType.all, - "kwargs": {"origin": np.array([0., 0., 0.])}, + "kwargs": {"neurite_type": NeuriteType.all, "origin": np.array([0., 0., 0.])}, "expected_wout_subtrees": 4.47213595499958, "expected_with_subtrees": 4.47213595499958, }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": 3.60555127546398, # [3, 3] - [0, 1] "expected_with_subtrees": 3.16227766016837, # [1, 4] - [0, 1] }, { - "neurite_type": NeuriteType.basal_dendrite, - "kwargs": {"origin": np.array([0., 0., 0.])}, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite, "origin": np.array([0., 0., 0.])}, "expected_wout_subtrees": 4.47213595499958, # [2, 4] - [0, 0] "expected_with_subtrees": 4.12310562561766, # [1, 4] - [0, 0] }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0.0, "expected_with_subtrees": 2.23606797749979, # [3, 3] - [1, 2] }, { - "neurite_type": NeuriteType.axon, - "kwargs": {"origin": np.array([0., 0., 0.])}, + "kwargs": {"neurite_type": NeuriteType.axon, "origin": np.array([0., 0., 0.])}, "expected_wout_subtrees": 0.0, "expected_with_subtrees": 4.47213595499958, # [2, 4] - [0, 0] } ], "total_length_per_neurite": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [3., 6.82842712474619, 3.], "expected_with_subtrees": [3., 3.414213562373095, 3.414213562373095, 3], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [3., 6.82842712474619], "expected_with_subtrees": [3., 3.414213562373095], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [3.414213562373095], }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": [3.], "expected_with_subtrees": [3.], } ], "total_area_per_neurite" : [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [1.884956, 4.290427, 1.884956], # total_length * 2piR "expected_with_subtrees": [1.884956, 2.145214, 2.145214, 1.884956], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [1.884956, 4.290427], "expected_with_subtrees": [1.884956, 2.145214], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [2.145214], }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": [1.884956], "expected_with_subtrees": [1.884956], } ], "total_volume_per_neurite": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [0.09424778, 0.21452136, 0.09424778], # total_length * piR^2 "expected_with_subtrees": [0.09424778, 0.10726068, 0.10726068, 0.09424778], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [0.09424778, 0.21452136], "expected_with_subtrees": [0.09424778, 0.10726068], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [0.10726068], }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": [0.09424778], "expected_with_subtrees": [0.09424778], } ], "trunk_origin_azimuths": [ # Not applicable to distal subtrees { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [3.1415927, 0.0, 0.0], "expected_with_subtrees": [3.1415927, 0.0, 0.0], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [3.1415927, 0.0], "expected_with_subtrees": [3.1415927, 0.0], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [], }, ], "trunk_origin_elevations": [ # Not applicable to distal subtrees { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [0.0, 1.5707964, -1.5707964], "expected_with_subtrees": [0.0, 1.5707964, -1.5707964], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [0.0, 1.5707964], "expected_with_subtrees": [0.0, 1.5707964], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [], }, ], "trunk_vectors": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [[-1., 0., 0.], [0., 1., 0.], [0., -1., 0.]], "expected_with_subtrees": [[-1., 0., 0.], [0., 1., 0.], [1., 2., 0.], [0., -1., 0.]], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [[-1., 0., 0.], [0., 1., 0.]], "expected_with_subtrees": [[-1., 0., 0.], [0., 1., 0.]], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [[1., 2., 0.]], }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": [[0., -1., 0.]], "expected_with_subtrees": [[0., -1., 0.]], }, @@ -246,24 +240,24 @@ def _morphology_features(): ], "trunk_angles": [ # Not applicable to distal subtrees { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [1.570796, 3.141592, 1.570796], "expected_with_subtrees": [1.570796, 3.141592, 1.570796], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [1.5707964, 1.570796], "expected_with_subtrees": [1.5707964, 1.570796], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [], }, ], "trunk_angles_from_vector": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [ [np.pi / 2., - np.pi / 2, np.pi], [0., 0., 0.], @@ -277,12 +271,12 @@ def _morphology_features(): ], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [[np.pi / 2., - np.pi / 2, np.pi], [0., 0., 0.]], "expected_with_subtrees": [[np.pi / 2., - np.pi / 2, np.pi], [0., 0., 0.]], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [[0.463648, -0.463648, 0.]], }, @@ -290,7 +284,6 @@ def _morphology_features(): ], "trunk_angles_inter_types": [ { - "neurite_type": None, "kwargs": { "source_neurite_type": NeuriteType.basal_dendrite, "target_neurite_type": NeuriteType.axon, @@ -304,140 +297,133 @@ def _morphology_features(): ], "trunk_origin_radii": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [0.1, 0.1, 0.1], "expected_with_subtrees": [0.1, 0.1, 0.1, 0.1], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [0.1, 0.1], "expected_with_subtrees": [0.1, 0.1], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [0.1], }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": [0.1], "expected_with_subtrees": [0.1], }, ], "trunk_section_lengths": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [1., 1.414213, 1.], "expected_with_subtrees": [1., 1.414213, 1.414213, 1.], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [1., 1.414213], "expected_with_subtrees": [1., 1.414213], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [1.414213], }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": [1.], "expected_with_subtrees": [1.], }, ], "number_of_neurites": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": 3, "expected_with_subtrees": 4, }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": 2, "expected_with_subtrees": 2, }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": 1, }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": 1, "expected_with_subtrees": 1, }, ], "neurite_volume_density": [ # our morphology is flat :( { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [np.nan, np.nan, np.nan], "expected_with_subtrees": [np.nan, np.nan, np.nan, np.nan], }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [np.nan, np.nan], "expected_with_subtrees": [np.nan, np.nan], }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], "expected_with_subtrees": [np.nan], }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": [np.nan], "expected_with_subtrees": [np.nan], }, ], "sholl_crossings": [ { - "neurite_type": NeuriteType.all, - "kwargs": {"radii": [1.5, 3.5]}, + "kwargs": {"neurite_type": NeuriteType.all, "radii": [1.5, 3.5]}, "expected_wout_subtrees": [3, 2], "expected_with_subtrees": [3, 2], }, { - "neurite_type": NeuriteType.basal_dendrite, - "kwargs": {"radii": [1.5, 3.5]}, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite, "radii": [1.5, 3.5]}, "expected_wout_subtrees": [2, 2], "expected_with_subtrees": [2, 1], }, { - "neurite_type": NeuriteType.axon, - "kwargs": {"radii": [1.5, 3.5]}, + "kwargs": {"neurite_type": NeuriteType.axon, "radii": [1.5, 3.5]}, "expected_wout_subtrees": [0, 0], "expected_with_subtrees": [0, 1], }, { - "neurite_type": NeuriteType.apical_dendrite, - "kwargs": {"radii": [1.5, 3.5]}, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite, "radii": [1.5, 3.5]}, "expected_wout_subtrees": [1, 0], "expected_with_subtrees": [1, 0], }, ], "sholl_frequency": [ { - "neurite_type": NeuriteType.all, - "kwargs": {"step_size": 3}, + "kwargs": {"neurite_type": NeuriteType.all, "step_size": 3}, "expected_wout_subtrees": [0, 2], "expected_with_subtrees": [0, 2], }, { - "neurite_type": NeuriteType.basal_dendrite, - "kwargs": {"step_size": 3}, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite, "step_size": 3}, "expected_wout_subtrees": [0, 2], "expected_with_subtrees": [0, 1], }, # { see #987 -# "neurite_type": NeuriteType.axon, +# "kwargs": {"neurite_type": NeuriteType.axon}, # "kwargs": {"step_size": 3}, # "expected_wout_subtrees": [0, 0], # "expected_with_subtrees": [0, 1], # }, { - "neurite_type": NeuriteType.apical_dendrite, - "kwargs": {"step_size": 2}, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite, "step_size": 2}, "expected_wout_subtrees": [0, 1], "expected_with_subtrees": [0, 1], }, @@ -445,66 +431,66 @@ def _morphology_features(): ], "total_width": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": 6.0, "expected_with_subtrees": 6.0, }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": 6.0, "expected_with_subtrees": 4.0, }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0.0, "expected_with_subtrees": 2.0, }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": 1.0, "expected_with_subtrees": 1.0, }, ], "total_height": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": 7.0, "expected_with_subtrees": 7.0, }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": 4.0, "expected_with_subtrees": 4.0, }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0.0, "expected_with_subtrees": 2.0, }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": 2.0, "expected_with_subtrees": 2.0, }, ], "total_depth": [ { - "neurite_type": NeuriteType.all, + "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": 0.0, "expected_with_subtrees": 0.0, }, { - "neurite_type": NeuriteType.basal_dendrite, + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": 0.0, "expected_with_subtrees": 0.0, }, { - "neurite_type": NeuriteType.axon, + "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0.0, "expected_with_subtrees": 0.0, }, { - "neurite_type": NeuriteType.apical_dendrite, + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, "expected_wout_subtrees": 0.0, "expected_with_subtrees": 0.0, }, @@ -521,17 +507,14 @@ def _morphology_features(): for feature_name, configurations in features.items(): for cfg in configurations: kwargs = cfg["kwargs"] if "kwargs" in cfg else {} - yield feature_name, cfg["neurite_type"], kwargs, cfg["expected_wout_subtrees"], cfg["expected_with_subtrees"] + yield feature_name, kwargs, cfg["expected_wout_subtrees"], cfg["expected_with_subtrees"] -@pytest.mark.parametrize("feature_name, neurite_type, kwargs, expected_wout_subtrees, expected_with_subtrees", _morphology_features()) -def test_features__morphology(feature_name, neurite_type, kwargs, expected_wout_subtrees, expected_with_subtrees, mixed_morph): +@pytest.mark.parametrize("feature_name, kwargs, expected_wout_subtrees, expected_with_subtrees", _morphology_features()) +def test_morphology__morphology_features(feature_name, kwargs, expected_wout_subtrees, expected_with_subtrees, mixed_morph): kwargs["use_subtrees"] = False - if neurite_type is not None: - kwargs["neurite_type"] = neurite_type - with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -542,6 +525,7 @@ def test_features__morphology(feature_name, neurite_type, kwargs, expected_wout_ ) kwargs["use_subtrees"] = True + npt.assert_allclose( get(feature_name, mixed_morph, **kwargs), expected_with_subtrees, From 40fe4ed74da131c0ad383fd4181a2099ca55f6aa Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 25 Feb 2022 17:14:18 +0100 Subject: [PATCH 46/87] Simplify subneurite iterators --- neurom/core/morphology.py | 55 ++++++++++-- neurom/features/morphology.py | 155 +++++++++++++++++----------------- tests/test_mixed.py | 73 +++++++++++++--- 3 files changed, 181 insertions(+), 102 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index f4bf56ff..d2f71184 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -211,7 +211,25 @@ def __repr__(self): NeuriteType.undefined: 4} -def iter_neurites(obj, mapfun=None, filt=None, neurite_order=NeuriteIter.FileOrder): +def _homogeneous_subtrees(neurite): + """Returns a dictionary the keys of which are section types and the values are the + sub-neurites. A sub-neurite can be either the entire tree or a homogeneous downstream + sub-tree. + Note: Only two different mixed types are allowed + """ + homogeneous_neurites = {neurite.root_node.type: neurite} + for section in neurite.root_node.ipreorder(): + if section.type not in homogeneous_neurites: + homogeneous_neurites[section.type] = Neurite(section.morphio_section) + if len(homogeneous_neurites) != 2: + raise TypeError( + f"Subtree types must be exactly two. Found {len(homogeneous_neurites)} instead.") + return list(homogeneous_neurites.values()) + + +def iter_neurites( + obj, mapfun=None, filt=None, neurite_order=NeuriteIter.FileOrder, use_subtrees=False +): """Iterator to a neurite, morphology or morphology population. Applies optional neurite filter and mapping functions. @@ -240,8 +258,19 @@ def iter_neurites(obj, mapfun=None, filt=None, neurite_order=NeuriteIter.FileOrd >>> 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) + def extract_subneurites(neurite): + if neurite.is_heterogeneous(): + return _homogeneous_subtrees(neurite) + return [neurite] + + 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' @@ -249,8 +278,19 @@ def iter_neurites(obj, mapfun=None, filt=None, neurite_order=NeuriteIter.FileOrd last_position = max(NRN_ORDER.values()) + 1 neurites = sorted(neurites, key=lambda neurite: NRN_ORDER.get(neurite.type, last_position)) + print("use_Subtrees:", use_subtrees) + if use_subtrees: + neurites = chain.from_iterable(map(extract_subneurites, neurites)) + neurite_iter = iter(neurites) if filt is None else filter(filt, neurites) - return neurite_iter if mapfun is None else map(mapfun, neurite_iter) + + if mapfun is None: + return neurite_iter + + if use_subtrees: + return (mapfun(neurite, section_type=neurite.type) for neurite in neurite_iter) + + return map(mapfun, neurite_iter) def iter_sections(neurites, @@ -283,11 +323,8 @@ def iter_sections(neurites, >>> filter = lambda n : n.type == nm.AXON >>> n_points = [len(s.points) for s in iter_sections(pop, neurite_filter=filter)] """ - sections = flatten( - iterator_type(neurite.root_node) for neurite in - iter_neurites(neurites, filt=neurite_filter, neurite_order=neurite_order) - ) - + neurites = iter_neurites(neurites, filt=neurite_filter, neurite_order=neurite_order) + sections = flatten(iterator_type(neurite.root_node) for neurite in neurites) return sections if section_filter is None else filter(section_filter, sections) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 064c88a5..f51e5a3f 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -66,66 +66,6 @@ feature = partial(feature, namespace=NameSpace.NEURON) -def _homogeneous_subtrees(neurite): - """Returns a dictionary the keys of which are section types and the values are the - sub-neurites. A sub-neurite can be either the entire tree or a homogeneous downstream - sub-tree. - Note: Only two different mixed types are allowed - """ - homogeneous_neurites = {neurite.root_node.type: neurite} - for section in neurite.root_node.ipreorder(): - if section.type not in homogeneous_neurites: - homogeneous_neurites[section.type] = Neurite(section.morphio_section) - if len(homogeneous_neurites) != 2: - raise TypeError( - f"Subtree types must be exactly two. Found {len(homogeneous_neurites)} instead.") - return homogeneous_neurites - - -def _iter_subneurites(obj, neurite_type=NeuriteType.all, use_subtrees=False): - - def extract_subneurites(neurite): - if neurite.is_heterogeneous(): - return [subtree for _, subtree in _homogeneous_subtrees(neurite).items()] - return [neurite] - - neurites = iter_neurites(obj) - - if use_subtrees: - neurites = chain.from_iterable(map(extract_subneurites, neurites)) - - yield from filter(is_type(neurite_type), neurites) - - -def _map_homogeneous_subtrees(function, neurite, neurite_type): - - check_type = is_type(neurite_type) - - for section_type, subtree in _homogeneous_subtrees(neurite).items(): - if check_type(subtree): - yield function(subtree, section_type=section_type) - - -def map_neurites(function, obj, neurite_type=NeuriteType.all, use_subtrees=False): - """ - If `use_subtrees` is enabled, each neurite that is inhomogeneous, is traversed and the - subtrees with their respective types are returned. For each of these subtrees the features - need to be calculated with a section filter, to ensure no sections from other subtrees are - traversed. - """ - check_type = is_type(neurite_type) - - if use_subtrees: - for neurite in iter_neurites(obj): - if neurite.is_heterogeneous(): - yield from _map_homogeneous_subtrees(function, neurite, neurite_type) - else: - if check_type(neurite): - yield function(neurite) - else: - yield from iter_neurites(obj, mapfun=function, filt=is_type(neurite_type)) - - @feature(shape=()) def soma_volume(morph): """Get the volume of a morphology's soma.""" @@ -151,9 +91,13 @@ def soma_radius(morph): @feature(shape=()) def max_radial_distance(morph, origin=None, neurite_type=NeuriteType.all, use_subtrees=False): """Get the maximum radial distances of the termination sections.""" - function = partial(nf.max_radial_distance, origin=origin) term_radial_distances = list( - map_neurites(function, morph, neurite_type, use_subtrees) + iter_neurites( + morph, + mapfun=partial(nf.max_radial_distance, origin=origin), + filt=is_type(neurite_type), + use_subtrees=use_subtrees + ) ) return max(term_radial_distances) if term_radial_distances else 0. @@ -161,14 +105,26 @@ def max_radial_distance(morph, origin=None, neurite_type=NeuriteType.all, use_su @feature(shape=(...,)) def number_of_sections_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """List of numbers of sections per neurite.""" - return list(map_neurites(nf.number_of_sections, morph, neurite_type, use_subtrees)) + return list( + iter_neurites( + morph, + mapfun=nf.number_of_sections, + filt=is_type(neurite_type), + use_subtrees=use_subtrees + ) + ) @feature(shape=(...,)) def total_length_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Neurite lengths.""" return list( - map_neurites(nf.total_length, morph, neurite_type, use_subtrees) + iter_neurites( + morph, + mapfun=nf.total_length, + filt=is_type(neurite_type), + use_subtrees=use_subtrees + ) ) @@ -176,7 +132,12 @@ def total_length_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=F def total_area_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Neurite areas.""" return list( - map_neurites(nf.total_area, morph, neurite_type, use_subtrees) + iter_neurites( + morph, + mapfun=nf.total_area, + filt=is_type(neurite_type), + use_subtrees=use_subtrees + ) ) @@ -184,7 +145,12 @@ def total_area_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=Fal def total_volume_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Neurite volumes.""" return list( - map_neurites(nf.total_volume, morph, neurite_type, use_subtrees) + iter_neurites( + morph, + mapfun=nf.total_volume, + filt=is_type(neurite_type), + use_subtrees=use_subtrees + ) ) @@ -203,7 +169,13 @@ def azimuth(neurite): morphmath.vector(neurite.root_node.points[0], morph.soma.center) ) - return _map_neurites(azimuth, morph, neurite_type) + return list( + iter_neurites( + morph, + mapfun=azimuth, + filt=is_type(neurite_type), + ) + ) @feature(shape=(...,)) @@ -222,16 +194,25 @@ def elevation(neurite): morphmath.vector(neurite.root_node.points[0], morph.soma.center) ) - return _map_neurites(elevation, morph, neurite_type) + return list( + iter_neurites( + morph, + mapfun=elevation, + filt=is_type(neurite_type), + ) + ) @feature(shape=(...,)) def trunk_vectors(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Calculate the vectors between all the trunks of the morphology and the soma center.""" - def vector_to_root_node(neurite): + def vector_from_soma_to_root(neurite): return morphmath.vector(neurite.root_node.points[0], morph.soma.center) - return _map_neurites(vector_to_root_node, morph, neurite_type, use_subtrees) + return [ + vector_from_soma_to_root(n) + for n in iter_neurites(morph, filt=is_type(neurite_type), use_subtrees=use_subtrees) + ] @feature(shape=(...,)) @@ -513,29 +494,40 @@ def trunk_mean_radius(neurite): else trunk_mean_radius ) - return map_neurites(function, morph, neurite_type, use_subtrees) + return [ + function(neu) + for neu in iter_neurites(morph, filt=is_type(neurite_type), use_subtrees=use_subtrees) + ] @feature(shape=(...,)) def trunk_section_lengths(morph, neurite_type=NeuriteType.all, use_subtrees=False): """List of lengths of trunk sections of neurites in a morph.""" - def trunk_section_length(neurite): - return morphmath.section_length(neurite.root_node.points) - - return [morphmath.section_length(n.root_node.points) - for n in _iter_subneurites(morph, neurite_type, use_subtrees)] + return [ + morphmath.section_length(n.root_node.points) + for n in iter_neurites(morph, filt=is_type(neurite_type), use_subtrees=use_subtrees) + ] @feature(shape=()) def number_of_neurites(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Number of neurites in a morph.""" - return len(map_neurites(lambda n: n, morph, neurite_type, use_subtrees)) + return sum( + 1 for _ in iter_neurites(morph, filt=is_type(neurite_type), use_subtrees=use_subtrees) + ) @feature(shape=(...,)) def neurite_volume_density(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Get volume density per neurite.""" - return list(map_neurites(nf.volume_density, morph, neurite_type, use_subtrees)) + return list( + iter_neurites( + morph, + mapfun=nf.volume_density, + filt=is_type(neurite_type), + use_subtrees=use_subtrees + ) + ) @feature(shape=(...,)) @@ -660,7 +652,12 @@ def iter_coordinates(neurite, section_type=NeuriteType.all): axis_coordinates = np.fromiter( chain.from_iterable( - map_neurites(iter_coordinates, morph, neurite_type, use_subtrees) + iter_neurites( + morph, + mapfun=iter_coordinates, + filt=is_type(neurite_type), + use_subtrees=use_subtrees + ) ), dtype=np.float32 ) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index bb67fce6..3dfe618d 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -5,7 +5,7 @@ import numpy.testing as npt from neurom import NeuriteType from neurom.features import get -from neurom.features import _MORPHOLOGY_FEATURES +from neurom.features import _MORPHOLOGY_FEATURES, _NEURITE_FEATURES @pytest.fixture def mixed_morph(): @@ -35,7 +35,7 @@ def mixed_morph(): reader="swc") -def _morphology_features(): +def _morphology_features(mode): features = { "soma_radius": [ @@ -504,13 +504,69 @@ def _morphology_features(): f"{features_not_tested}" ) + for feature_name, configurations in features.items(): + for cfg in configurations: + kwargs = cfg["kwargs"] if "kwargs" in cfg else {} + + if mode == "with-subtrees": + expected = cfg["expected_with_subtrees"] + elif mode == "wout-subtrees": + expected = cfg["expected_wout_subtrees"] + else: + raise ValueError("Uknown mode") + + yield feature_name, kwargs, expected + + +@pytest.mark.parametrize("feature_name, kwargs, expected", _morphology_features(mode="wout-subtrees")) +def test_morphology__morphology_features_wout_subtrees(feature_name, kwargs, expected, mixed_morph): + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + kwargs["use_subtrees"] = False + + npt.assert_allclose( + get(feature_name, mixed_morph, **kwargs), + expected, + rtol=1e-6 + ) + + +@pytest.mark.parametrize("feature_name, kwargs, expected", _morphology_features(mode="with-subtrees")) +def test_morphology__morphology_features_with_subtrees(feature_name, kwargs, expected, mixed_morph): + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + kwargs["use_subtrees"] = True + + npt.assert_allclose( + get(feature_name, mixed_morph, **kwargs), + expected, + rtol=1e-6 + ) + +""" +def _neurite_features(): + + features = {} + + features_not_tested = set(_NEURITE_FEATURES) - set(features.keys()) + + #assert not features_not_tested, ( + # "The following morphology tests need to be included in the mixed morphology tests:\n" + # f"{features_not_tested}" + #) + for feature_name, configurations in features.items(): for cfg in configurations: kwargs = cfg["kwargs"] if "kwargs" in cfg else {} yield feature_name, kwargs, cfg["expected_wout_subtrees"], cfg["expected_with_subtrees"] -@pytest.mark.parametrize("feature_name, kwargs, expected_wout_subtrees, expected_with_subtrees", _morphology_features()) + +@pytest.mark.parametrize("feature_name, kwargs, expected_wout_subtrees, expected_with_subtrees", _neurite_features()) def test_morphology__morphology_features(feature_name, kwargs, expected_wout_subtrees, expected_with_subtrees, mixed_morph): kwargs["use_subtrees"] = False @@ -532,16 +588,5 @@ def test_morphology__morphology_features(feature_name, kwargs, expected_wout_sub rtol=1e-6 ) -""" -def test_mixed_types(mixed_morph): - - from neurom import NeuriteType - from neurom.features import get - - types = [neurite.type for neurite in mixed_morph.neurites] - - res = get("number_of_sections", mixed_morph, neurite_type=NeuriteType.axon) - print(types, res) - assert False """ From 6b0f7f82171020b7ec13b847e07d50a2caf93659 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 25 Feb 2022 17:17:40 +0100 Subject: [PATCH 47/87] Remove print --- neurom/core/morphology.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index d2f71184..c52107e4 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -278,7 +278,6 @@ def extract_subneurites(neurite): last_position = max(NRN_ORDER.values()) + 1 neurites = sorted(neurites, key=lambda neurite: NRN_ORDER.get(neurite.type, last_position)) - print("use_Subtrees:", use_subtrees) if use_subtrees: neurites = chain.from_iterable(map(extract_subneurites, neurites)) From 8b8f9a9511b9b1fcbbe4cb0941591f68694c5fd1 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sat, 26 Feb 2022 01:40:09 +0100 Subject: [PATCH 48/87] Convert some of the neurite features --- neurom/features/__init__.py | 28 +- neurom/features/neurite.py | 56 ++-- tests/test_mixed.py | 492 +++++++++++++++++++++++++++++++++--- 3 files changed, 504 insertions(+), 72 deletions(-) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 3679a4e8..0f8da7b5 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -40,7 +40,7 @@ import inspect import operator from enum import Enum -from functools import reduce +from functools import reduce, partial from neurom.core import Population, Morphology, Neurite from neurom.core.morphology import iter_neurites @@ -66,12 +66,22 @@ def _flatten_feature(feature_shape, feature_value): return reduce(operator.concat, feature_value, []) -def _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs): +def _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs, use_subtrees): """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 []) + + return reduce( + operator.add, + ( + iter_neurites( + obj, + mapfun=partial(feature_, **kwargs), + filt=neurite_filter, + use_subtrees=use_subtrees, + ) + ), + 0 if feature_.shape == () else [] + ) def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs): @@ -126,7 +136,7 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) elif feature_name in _NEURITE_FEATURES: feature_ = _NEURITE_FEATURES[feature_name] - res = _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs) + res = _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs, use_subtrees) elif isinstance(obj, Population) or (is_obj_list and isinstance(obj[0], Morphology)): # input is a morphology population or a list of morphs @@ -140,7 +150,11 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) feature_ = _NEURITE_FEATURES[feature_name] res = _flatten_feature( feature_.shape, - [_get_neurites_feature_value(feature_, n, neurite_filter, kwargs) for n in obj]) + [ + _get_neurites_feature_value(feature_, n, neurite_filter, kwargs, use_subtrees) + 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, ' diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 4d074289..8262852f 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -89,9 +89,11 @@ def number_of_sections(neurite, iterator_type=Section.ipreorder, section_type=Ne @feature(shape=()) -def number_of_bifurcations(neurite): +def number_of_bifurcations(neurite, section_type=NeuriteType.all): """Number of bf points.""" - return number_of_sections(neurite, iterator_type=Section.ibifurcation_point) + return number_of_sections( + neurite, iterator_type=Section.ibifurcation_point, section_type=section_type + ) @feature(shape=()) @@ -101,9 +103,9 @@ def number_of_forking_points(neurite): @feature(shape=()) -def number_of_leaves(neurite): +def number_of_leaves(neurite, section_type=NeuriteType.all): """Number of leaves points.""" - return number_of_sections(neurite, iterator_type=Section.ileaf) + return number_of_sections(neurite, iterator_type=Section.ileaf, section_type=section_type) @feature(shape=()) @@ -134,15 +136,15 @@ def _section_length(section): @feature(shape=(...,)) -def section_lengths(neurite): +def section_lengths(neurite, section_type=NeuriteType.all): """Section lengths.""" - return _map_sections(sf.section_length, neurite) + return _map_sections(_section_length, neurite, section_type=section_type) @feature(shape=(...,)) -def section_term_lengths(neurite): +def section_term_lengths(neurite, section_type=NeuriteType.all): """Termination section lengths.""" - return _map_sections(sf.section_length, neurite, Section.ileaf) + return _map_sections(_section_length, neurite, Section.ileaf, section_type) @feature(shape=(...,)) @@ -185,7 +187,7 @@ def pl2(node): ################################################################################ -def _map_segments(func, neurite): +def _map_segments(func, neurite, section_type=NeuriteType.all): """Map `func` to all the segments. `func` accepts a section and returns list of values corresponding to each segment. @@ -198,36 +200,36 @@ def _map_segments(func, neurite): @feature(shape=(...,)) -def segment_lengths(neurite): +def segment_lengths(neurite, section_type=NeuriteType.all): """Lengths of the segments.""" - return _map_segments(sf.segment_lengths, neurite) + return _map_segments(sf.segment_lengths, neurite, section_type=section_type) @feature(shape=(...,)) -def segment_areas(neurite): +def segment_areas(neurite, section_type=NeuriteType.all): """Areas of the segments.""" - return _map_segments(sf.segment_areas, neurite) + return _map_segments(sf.segment_areas, neurite, section_type) @feature(shape=(...,)) -def segment_volumes(neurite): +def segment_volumes(neurite, section_type=NeuriteType.all): """Volumes of the segments.""" - return _map_segments(sf.segment_volumes, neurite) + return _map_segments(sf.segment_volumes, neurite, section_type=section_type) @feature(shape=(...,)) -def segment_radii(neurite): +def segment_radii(neurite, section_type=NeuriteType.all): """Arithmetic mean of the radii of the points in segments.""" - return _map_segments(sf.segment_mean_radii, neurite) + return _map_segments(sf.segment_radii, neurite, section_type=section_type) @feature(shape=(...,)) -def segment_taper_rates(neurite): +def segment_taper_rates(neurite, section_type=NeuriteType.all): """Diameters taper rates of the segments. The taper rate is defined as the absolute radii differences divided by length of the section """ - return _map_segments(sf.segment_taper_rates, neurite) + return _map_segments(sf.segment_taper_rates, neurite, section_type=section_type) @feature(shape=(...,)) @@ -457,27 +459,27 @@ def volume_density(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) -def section_volumes(neurite): +def section_volumes(neurite, section_type=NeuriteType.all): """Section volumes.""" - return _map_sections(sf.section_volume, neurite) + return _map_sections(sf.section_volume, neurite, section_type=section_type) @feature(shape=(...,)) -def section_areas(neurite): +def section_areas(neurite, section_type=NeuriteType.all): """Section areas.""" - return _map_sections(sf.section_area, neurite) + return _map_sections(sf.section_area, neurite, section_type=section_type) @feature(shape=(...,)) -def section_tortuosity(neurite): +def section_tortuosity(neurite, section_type=NeuriteType.all): """Section tortuosities.""" - return _map_sections(sf.section_tortuosity, neurite) + return _map_sections(sf.section_tortuosity, neurite, section_type=section_type) @feature(shape=(...,)) -def section_end_distances(neurite): +def section_end_distances(neurite, section_type=NeuriteType.all): """Section end to end distances.""" - return _map_sections(sf.section_end_distance, neurite) + return _map_sections(sf.section_end_distance, neurite, section_type=section_type) @feature(shape=(...,)) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 3dfe618d..8b71ad53 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -35,6 +35,22 @@ def mixed_morph(): reader="swc") +def _dispatch_features(features, mode): + + for feature_name, configurations in features.items(): + for cfg in configurations: + kwargs = cfg["kwargs"] if "kwargs" in cfg else {} + + if mode == "with-subtrees": + expected = cfg["expected_with_subtrees"] + elif mode == "wout-subtrees": + expected = cfg["expected_wout_subtrees"] + else: + raise ValueError("Uknown mode") + + yield feature_name, kwargs, expected + + def _morphology_features(mode): features = { @@ -504,18 +520,7 @@ def _morphology_features(mode): f"{features_not_tested}" ) - for feature_name, configurations in features.items(): - for cfg in configurations: - kwargs = cfg["kwargs"] if "kwargs" in cfg else {} - - if mode == "with-subtrees": - expected = cfg["expected_with_subtrees"] - elif mode == "wout-subtrees": - expected = cfg["expected_wout_subtrees"] - else: - raise ValueError("Uknown mode") - - yield feature_name, kwargs, expected + return _dispatch_features(features, mode) @pytest.mark.parametrize("feature_name, kwargs, expected", _morphology_features(mode="wout-subtrees")) @@ -524,8 +529,6 @@ def test_morphology__morphology_features_wout_subtrees(feature_name, kwargs, exp with warnings.catch_warnings(): warnings.simplefilter("ignore") - kwargs["use_subtrees"] = False - npt.assert_allclose( get(feature_name, mixed_morph, **kwargs), expected, @@ -539,18 +542,431 @@ def test_morphology__morphology_features_with_subtrees(feature_name, kwargs, exp with warnings.catch_warnings(): warnings.simplefilter("ignore") - kwargs["use_subtrees"] = True - npt.assert_allclose( - get(feature_name, mixed_morph, **kwargs), + get(feature_name, mixed_morph, use_subtrees=True, **kwargs), expected, rtol=1e-6 ) -""" -def _neurite_features(): +def _neurite_features(mode): - features = {} + features = { + "number_of_segments": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 11, + "expected_with_subtrees": 11, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 8, + "expected_with_subtrees": 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0, + "expected_with_subtrees": 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 3, + "expected_with_subtrees": 3, + }, + ], + "number_of_leaves": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 7, + "expected_with_subtrees": 7, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 5, + "expected_with_subtrees": 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0, + "expected_with_subtrees": 2, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 2, + "expected_with_subtrees": 2, + }, + ], + "total_length": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 12.828427, + "expected_with_subtrees": 12.828427, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 9.828427, + "expected_with_subtrees": 6.414214, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0., + "expected_with_subtrees": 3.414214, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 3., + "expected_with_subtrees": 3., + } + ], + "total_area": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 8.060339, + "expected_with_subtrees": 8.060339, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 6.175383, + "expected_with_subtrees": 4.030170, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0., + "expected_with_subtrees": 2.145214, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 1.884956, + "expected_with_subtrees": 1.884956, + } + ], + "total_volume": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 0.403016, + "expected_with_subtrees": 0.403016, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 0.308769, + "expected_with_subtrees": 0.201508, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0., + "expected_with_subtrees": 0.107261, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 0.0942478, + "expected_with_subtrees": 0.0942478, + } + ], + "section_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + "expected_with_subtrees": + [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": + [1., 1., 1., 1.414214, 2., 1.414214, 1., 1.], + "expected_with_subtrees": + [1., 1., 1., 1.414214, 2.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0., + "expected_with_subtrees": [1.414214, 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1., 1., 1.], + "expected_with_subtrees": [1., 1., 1.], + } + ], + "section_areas": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, + 0.628318, 0.628318, 0.628318, 0.628318, 0.628318], + "expected_with_subtrees": + [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, + 0.628318, 0.628318, 0.628318, 0.628318, 0.628318], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": + [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, + 0.628318, 0.628318], + "expected_with_subtrees": + [0.628318, 0.628319, 0.628319, 0.888577, 1.256637], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.888576, 0.628318, 0.628318], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.628318, 0.628318, 0.628318], + "expected_with_subtrees": [0.628318, 0.628318, 0.628318], + } + + ], + "section_volumes": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, + 0.031415, 0.031415, 0.031415, 0.031415], + "expected_with_subtrees": + [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, + 0.031415, 0.031415, 0.031415, 0.031415], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": + [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, + 0.031415], + "expected_with_subtrees": + [0.031415, 0.031415, 0.031415, 0.044428, 0.062831], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.044428, 0.031415, 0.031415], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.031415, 0.031415, 0.031415], + "expected_with_subtrees": [0.031415, 0.031415, 0.031415], + } + ], + "section_tortuosity": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1.0] * 11, + "expected_with_subtrees": [1.0] * 11, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1.0] * 8, + "expected_with_subtrees": [1.0] * 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.0] * 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.0] * 3, + "expected_with_subtrees": [1.0] * 3, + } + ], + "section_end_distances": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + "expected_with_subtrees": + [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": + [1., 1., 1., 1.414214, 2., 1.414214, 1., 1.], + "expected_with_subtrees": + [1., 1., 1., 1.414214, 2.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0., + "expected_with_subtrees": [1.414214, 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1., 1., 1.], + "expected_with_subtrees": [1., 1., 1.], + } + ], + "section_term_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1., 1., 2., 1., 1., 1., 1.], + "expected_with_subtrees": [1., 1., 2., 1., 1., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1., 1., 2., 1., 1.], + "expected_with_subtrees": [1., 1., 2.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0., + "expected_with_subtrees": [1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1., 1.], + "expected_with_subtrees": [1., 1.], + } + ], + "segment_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + "expected_with_subtrees": + [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": + [1., 1., 1., 1.414214, 2., 1.414214, 1., 1.], + "expected_with_subtrees": + [1., 1., 1., 1.414214, 2.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0., + "expected_with_subtrees": [1.414214, 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1., 1., 1.], + "expected_with_subtrees": [1., 1., 1.], + } + ], + "segment_areas": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, + 0.628318, 0.628318, 0.628318, 0.628318, 0.628318], + "expected_with_subtrees": + [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, + 0.628318, 0.628318, 0.628318, 0.628318, 0.628318], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": + [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, + 0.628318, 0.628318], + "expected_with_subtrees": + [0.628318, 0.628319, 0.628319, 0.888577, 1.256637], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.888576, 0.628318, 0.628318], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.628318, 0.628318, 0.628318], + "expected_with_subtrees": [0.628318, 0.628318, 0.628318], + } + ], + "segment_volumes": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, + 0.031415, 0.031415, 0.031415, 0.031415], + "expected_with_subtrees": + [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, + 0.031415, 0.031415, 0.031415, 0.031415], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": + [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, + 0.031415], + "expected_with_subtrees": + [0.031415, 0.031415, 0.031415, 0.044428, 0.062831], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.044428, 0.031415, 0.031415], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.031415, 0.031415, 0.031415], + "expected_with_subtrees": [0.031415, 0.031415, 0.031415], + } + ], + "segment_radii": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0.1] * 11, + "expected_with_subtrees": [0.1] * 11, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [0.1] * 8, + "expected_with_subtrees": [0.1] * 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.1] * 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.1] * 3, + "expected_with_subtrees": [0.1] * 3, + } + ], + "segment_taper_rates": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0.0] * 11, + "expected_with_subtrees": [0.0] * 11, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [0.0] * 8, + "expected_with_subtrees": [0.0] * 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.0] * 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.0] * 3, + "expected_with_subtrees": [0.0] * 3, + }, + ], +# "number_of_bifurcations": [ +# { +# "kwargs": {"neurite_type": NeuriteType.all}, +# "expected_wout_subtrees": 4, +# "expected_with_subtrees": 4, +# }, +# { +# "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, +# "expected_wout_subtrees": 3, +# "expected_with_subtrees": 1, +# }, +# { +# "kwargs": {"neurite_type": NeuriteType.axon}, +# "expected_wout_subtrees": 0, +# "expected_with_subtrees": 1, +# }, +# { +# "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, +# "expected_wout_subtrees": 1, +# "expected_with_subtrees": 1, +# }, +# ] + } features_not_tested = set(_NEURITE_FEATURES) - set(features.keys()) @@ -559,34 +975,34 @@ def _neurite_features(): # f"{features_not_tested}" #) - for feature_name, configurations in features.items(): - for cfg in configurations: - kwargs = cfg["kwargs"] if "kwargs" in cfg else {} - yield feature_name, kwargs, cfg["expected_wout_subtrees"], cfg["expected_with_subtrees"] - + return _dispatch_features(features, mode) -@pytest.mark.parametrize("feature_name, kwargs, expected_wout_subtrees, expected_with_subtrees", _neurite_features()) -def test_morphology__morphology_features(feature_name, kwargs, expected_wout_subtrees, expected_with_subtrees, mixed_morph): - - kwargs["use_subtrees"] = False +@pytest.mark.parametrize( + "feature_name, kwargs, expected", _neurite_features(mode="wout-subtrees") +) +def test_morphology__neurite_features_wout_subtrees(feature_name, kwargs, expected, mixed_morph): with warnings.catch_warnings(): warnings.simplefilter("ignore") npt.assert_allclose( get(feature_name, mixed_morph, **kwargs), - expected_wout_subtrees, - rtol=1e-6 + expected, + atol=1e-6 ) - kwargs["use_subtrees"] = True - npt.assert_allclose( - get(feature_name, mixed_morph, **kwargs), - expected_with_subtrees, - rtol=1e-6 - ) +@pytest.mark.parametrize( + "feature_name, kwargs, expected", _neurite_features(mode="with-subtrees") +) +def test_morphology__neurite_features_with_subtrees(feature_name, kwargs, expected, mixed_morph): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") -""" + npt.assert_allclose( + get(feature_name, mixed_morph, use_subtrees=True, **kwargs), + expected, + atol=1e-6 + ) From f1f34781e00aea3383ddf44faa3ea9000d5140ff Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sat, 26 Feb 2022 13:37:40 +0100 Subject: [PATCH 49/87] Handle forking points with mixed subtrees --- neurom/core/morphology.py | 4 ++ neurom/features/neurite.py | 23 +++++++--- tests/test_mixed.py | 88 ++++++++++++++++++++++++++++---------- 3 files changed, 88 insertions(+), 27 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index c52107e4..ab4c5797 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -75,6 +75,10 @@ def append_section(self, section): return self.morphio_section.append_section(section.morphio_section) return self.morphio_section.append_section(section) + def is_homogeneous_point(self): + """A section is homogeneous if it has the same type with its children""" + return all(c.type == self.type for c in self.children) + def is_forking_point(self): """Is this section a forking point?""" return len(self.children) > 1 diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 8262852f..6abe2734 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -63,7 +63,18 @@ def _map_sections(fun, neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Map `fun` to all the sections.""" - return list(map(fun, filter(is_type(section_type), iterator_type(neurite.root_node)))) + + check_type = is_type(section_type) + + if ( + iterator_type in (Section.ibifurcation_point, Section.iforking_point) + and section_type != NeuriteType.all + ): + filt = lambda s: check_type(s) and Section.is_homogeneous_point(s) + else: + filt = check_type + + return list(map(fun, filter(filt, iterator_type(neurite.root_node)))) @feature(shape=()) @@ -97,9 +108,11 @@ def number_of_bifurcations(neurite, section_type=NeuriteType.all): @feature(shape=()) -def number_of_forking_points(neurite): +def number_of_forking_points(neurite, section_type=NeuriteType.all): """Number of forking points.""" - return number_of_sections(neurite, iterator_type=Section.iforking_point) + return number_of_sections( + neurite, iterator_type=Section.iforking_point, section_type=section_type + ) @feature(shape=()) @@ -148,9 +161,9 @@ def section_term_lengths(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) -def section_bif_lengths(neurite): +def section_bif_lengths(neurite, section_type=NeuriteType.all): """Bifurcation section lengths.""" - return _map_sections(sf.section_length, neurite, Section.ibifurcation_point) + return _map_sections(sf.section_length, neurite, Section.ibifurcation_point, section_type) @feature(shape=(...,)) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 8b71ad53..d886d0f1 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -944,28 +944,72 @@ def _neurite_features(mode): "expected_with_subtrees": [0.0] * 3, }, ], -# "number_of_bifurcations": [ -# { -# "kwargs": {"neurite_type": NeuriteType.all}, -# "expected_wout_subtrees": 4, -# "expected_with_subtrees": 4, -# }, -# { -# "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, -# "expected_wout_subtrees": 3, -# "expected_with_subtrees": 1, -# }, -# { -# "kwargs": {"neurite_type": NeuriteType.axon}, -# "expected_wout_subtrees": 0, -# "expected_with_subtrees": 1, -# }, -# { -# "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, -# "expected_wout_subtrees": 1, -# "expected_with_subtrees": 1, -# }, -# ] + "number_of_bifurcations": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 4, + "expected_with_subtrees": 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 3, + "expected_with_subtrees": 1, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0, + "expected_with_subtrees": 1, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 1, + "expected_with_subtrees": 1, + }, + ], + "number_of_forking_points": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 4, + "expected_with_subtrees": 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 3, + "expected_with_subtrees": 1, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0, + "expected_with_subtrees": 1, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 1, + "expected_with_subtrees": 1, + }, + ], + "section_bif_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1., 1.414214, 1.414214, 1.], + "expected_with_subtrees": [1., 1.414214, 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1., 1.414214, 1.414214], + "expected_with_subtrees": [1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.414214], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.], + "expected_with_subtrees": [1.], + } + ], } features_not_tested = set(_NEURITE_FEATURES) - set(features.keys()) From 5f1e142656c81dffdb84539bfdc1b2ce02758455 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 27 Feb 2022 12:28:29 +0100 Subject: [PATCH 50/87] Convert more neurite features --- neurom/features/neurite.py | 102 ++++--- tests/test_mixed.py | 576 +++++++++++++++++++++++++++++++++---- 2 files changed, 580 insertions(+), 98 deletions(-) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 6abe2734..efb63c8d 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -167,21 +167,23 @@ def section_bif_lengths(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) -def section_branch_orders(neurite): +def section_branch_orders(neurite, section_type=NeuriteType.all): """Section branch orders.""" - return _map_sections(sf.branch_order, neurite) + return _map_sections(sf.branch_order, neurite, section_type=section_type) @feature(shape=(...,)) -def section_bif_branch_orders(neurite): +def section_bif_branch_orders(neurite, section_type=NeuriteType.all): """Bifurcation section branch orders.""" - return _map_sections(sf.branch_order, neurite, Section.ibifurcation_point) + return _map_sections( + sf.branch_order, neurite, Section.ibifurcation_point, section_type=section_type + ) @feature(shape=(...,)) -def section_term_branch_orders(neurite): +def section_term_branch_orders(neurite, section_type=NeuriteType.all): """Termination section branch orders.""" - return _map_sections(sf.branch_order, neurite, Section.ileaf) + return _map_sections(sf.branch_order, neurite, Section.ileaf, section_type=section_type) @feature(shape=(...,)) @@ -221,7 +223,7 @@ def segment_lengths(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) def segment_areas(neurite, section_type=NeuriteType.all): """Areas of the segments.""" - return _map_segments(sf.segment_areas, neurite, section_type) + return _map_segments(sf.segment_areas, neurite, section_type=section_type) @feature(shape=(...,)) @@ -246,25 +248,25 @@ def segment_taper_rates(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) -def section_taper_rates(neurite): +def section_taper_rates(neurite, section_type=NeuriteType.all): """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. """ - return _map_sections(sf.taper_rate, neurite) + return _map_sections(sf.taper_rate, neurite, section_type=section_type) @feature(shape=(...,)) -def segment_meander_angles(neurite): +def segment_meander_angles(neurite, section_type=NeuriteType.all): """Inter-segment opening angles in a section.""" - return _map_segments(sf.section_meander_angles, neurite) + return _map_segments(sf.section_meander_angles, neurite, section_type=section_type) @feature(shape=(..., 3)) -def segment_midpoints(neurite): +def segment_midpoints(neurite, section_type=NeuriteType.all): """Return a list of segment mid-points.""" - return _map_segments(sf.segment_midpoints, neurite) + return _map_segments(sf.segment_midpoints neurite, section_type=section_type) @feature(shape=(...,)) @@ -284,7 +286,7 @@ def segments_pathlength(section): @feature(shape=(...,)) -def segment_radial_distances(neurite, origin=None): +def segment_radial_distances(neurite, origin=None, section_type=NeuriteType.all): """Returns the list of distances between all segment mid points and origin.""" pos = neurite.root_node.points[0] if origin is None else origin @@ -293,23 +295,29 @@ def radial_distances(section): mid_pts = 0.5 * (section.points[:-1, COLS.XYZ] + section.points[1:, COLS.XYZ]) return np.linalg.norm(mid_pts - pos[COLS.XYZ], axis=1) - return _map_segments(radial_distances, neurite) + return _map_segments(_radial_distances, neurite, section_type=section_type) @feature(shape=(...,)) -def local_bifurcation_angles(neurite): +def local_bifurcation_angles(neurite, section_type=NeuriteType.all): """Get a list of local bf angles.""" - return _map_sections(bf.local_bifurcation_angle, - neurite, - iterator_type=Section.ibifurcation_point) + return _map_sections( + bf.local_bifurcation_angle, + neurite, + iterator_type=Section.ibifurcation_point, + section_type=section_type, + ) @feature(shape=(...,)) -def remote_bifurcation_angles(neurite): +def remote_bifurcation_angles(neurite, section_type=NeuriteType.all): """Get a list of remote bf angles.""" - return _map_sections(bf.remote_bifurcation_angle, - neurite, - iterator_type=Section.ibifurcation_point) + return _map_sections( + bf.remote_bifurcation_angle, + neurite, + iterator_type=Section.ibifurcation_point, + section_type=section_type, + ) @feature(shape=(...,)) @@ -353,15 +361,15 @@ def partition_asymmetry_length(neurite, method='petilla'): @feature(shape=(...,)) -def bifurcation_partitions(neurite): +def bifurcation_partitions(neurite, section_type=NeuriteType.all): """Partition at bf points.""" - return _map_sections(bf.bifurcation_partition, - neurite, - Section.ibifurcation_point) + return _map_sections( + bf.bifurcation_partition, neurite, Section.ibifurcation_point, section_type=section_type + ) @feature(shape=(...,)) -def sibling_ratios(neurite, method='first'): +def sibling_ratios(neurite, method='first', section_type=NeuriteType.all): """Sibling ratios at bf points. The sibling ratio is the ratio between the diameters of the @@ -369,25 +377,28 @@ def sibling_ratios(neurite, method='first'): 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) + return _map_sections( + partial(bf.sibling_ratio, method=method), + neurite, + Section.ibifurcation_point, + section_type=section_type, + ) @feature(shape=(..., 2)) -def partition_pairs(neurite): +def partition_pairs(neurite, section_type=NeuriteType.all): """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) + return _map_sections( + bf.partition_pair, neurite, Section.ibifurcation_point, section_type=section_type + ) @feature(shape=(...,)) -def diameter_power_relations(neurite, method='first'): +def diameter_power_relations(neurite, method='first', section_type=NeuriteType.all): """Calculate the diameter power relation at a bf point. Diameter power relation is defined in https://www.ncbi.nlm.nih.gov/pubmed/18568015 @@ -395,9 +406,12 @@ def diameter_power_relations(neurite, method='first'): 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) + return _map_sections( + partial(bf.diameter_power_relation, method=method), + neurite, + Section.ibifurcation_point, + section_type=section_type, + ) @feature(shape=(...,)) @@ -423,9 +437,11 @@ def section_term_radial_distances(neurite, origin=None, section_type=NeuriteType @feature(shape=(...,)) -def section_bif_radial_distances(neurite, origin=None): +def section_bif_radial_distances(neurite, origin=None, section_type=NeuriteType.all): """Get the radial distances of the bf sections.""" - return section_radial_distances(neurite, origin, Section.ibifurcation_point) + return section_radial_distances( + neurite, origin, Section.ibifurcation_point, section_type=section_type + ) @feature(shape=(...,)) @@ -507,6 +523,6 @@ def principal_direction_extents(neurite, direction=0): @feature(shape=(...,)) -def section_strahler_orders(neurite): +def section_strahler_orders(neurite, section_type=NeuriteType.all): """Inter-segment opening angles in a section.""" - return _map_sections(sf.strahler_order, neurite) + return _map_sections(sf.strahler_order, neurite, section_type=section_type) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index d886d0f1..3c7b8a65 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -6,6 +6,7 @@ from neurom import NeuriteType from neurom.features import get from neurom.features import _MORPHOLOGY_FEATURES, _NEURITE_FEATURES +import collections.abc @pytest.fixture def mixed_morph(): @@ -34,6 +35,46 @@ def mixed_morph(): """, reader="swc") +def _assert_feature_equal(obj, feature_name, expected_values, kwargs, use_subtrees): + + def innermost_value(iterable): + while isinstance(iterable, collections.abc.Iterable): + try: + iterable = iterable[0] + except IndexError: + # empty list + return None + return iterable + + + assert_equal = lambda a, b: npt.assert_equal( + a, b, err_msg=f"ACTUAL: {a}\nDESIRED: {b}", verbose=False + ) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + values = get(feature_name, obj, use_subtrees=use_subtrees, **kwargs) + + # handle empty lists because allclose always passes in that case. + # See: https://github.com/numpy/numpy/issues/11071 + if isinstance(values, collections.abc.Iterable): + if isinstance(expected_values, collections.abc.Iterable): + if isinstance(innermost_value(values), (float, np.floating)): + npt.assert_allclose(values, expected_values, atol=1e-5) + else: + assert_equal(values, expected_values) + else: + assert_equal(values, expected_values) + else: + if isinstance(expected_values, collections.abc.Iterable): + assert_equal(values, expected_values) + else: + if isinstance(values, (float, np.floating)): + npt.assert_allclose(values, expected_values, atol=1e-5) + else: + assert_equal(values, expected_values) + def _dispatch_features(features, mode): @@ -368,7 +409,7 @@ def _morphology_features(mode): }, { "kwargs": {"neurite_type": NeuriteType.axon}, - "expected_wout_subtrees": [], + "expected_wout_subtrees": 0, "expected_with_subtrees": 1, }, { @@ -525,28 +566,15 @@ def _morphology_features(mode): @pytest.mark.parametrize("feature_name, kwargs, expected", _morphology_features(mode="wout-subtrees")) def test_morphology__morphology_features_wout_subtrees(feature_name, kwargs, expected, mixed_morph): - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - npt.assert_allclose( - get(feature_name, mixed_morph, **kwargs), - expected, - rtol=1e-6 - ) + _assert_feature_equal(mixed_morph, feature_name, expected, kwargs, use_subtrees=False) @pytest.mark.parametrize("feature_name, kwargs, expected", _morphology_features(mode="with-subtrees")) -def test_morphology__morphology_features_with_subtrees(feature_name, kwargs, expected, mixed_morph): +def test_morphology__morphology_features_with_subtrees( + feature_name, kwargs, expected, mixed_morph +): + _assert_feature_equal(mixed_morph, feature_name, expected, kwargs, use_subtrees=True) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - npt.assert_allclose( - get(feature_name, mixed_morph, use_subtrees=True, **kwargs), - expected, - rtol=1e-6 - ) def _neurite_features(mode): @@ -678,7 +706,7 @@ def _neurite_features(mode): }, { "kwargs": {"neurite_type": NeuriteType.axon}, - "expected_wout_subtrees": 0., + "expected_wout_subtrees": [], "expected_with_subtrees": [1.414214, 1., 1.], }, { @@ -768,6 +796,88 @@ def _neurite_features(mode): "expected_with_subtrees": [1.0] * 3, } ], + "section_radial_distances": [ + { + # radial distances change when the mixed subtrees are processed because + # the root of the subtree is considered + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [1.0, 2.0, 1.4142135, 1.4142135, 3.1622777, 2.828427, + 3.6055512, 3.6055512, 1.0, 2.0, 1.4142135], + "expected_with_subtrees": + [1.0, 2.0, 1.4142135, 1.4142135, 3.1622777, 1.414214, + 2.236068, 2.236068, 1.0, 2.0, 1.4142135], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": + [1.0, 2.0, 1.4142135, 1.4142135, 3.1622777, 2.828427, + 3.6055512, 3.6055512], + "expected_with_subtrees": + [1.0, 2.0, 1.4142135, 1.4142135, 3.1622777], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.414214, 2.236068, 2.236068], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1., 2., 1.414214], + "expected_with_subtrees": [1., 2., 1.414214], + } + + ], + "section_term_radial_distances": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [2.0, 1.4142135, 3.1622777, 3.6055512, 3.6055512, 2.0, 1.4142135], + "expected_with_subtrees": + [2.0, 1.4142135, 3.1622777, 2.236068, 2.236068, 2.0, 1.4142135], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [2.0, 1.4142135, 3.1622777, 3.6055512, 3.6055512], + "expected_with_subtrees": [2.0, 1.4142135, 3.1622777], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [2.236068, 2.236068], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [2., 1.414214], + "expected_with_subtrees": [2., 1.414214], + } + + ], + "section_bif_radial_distances": [ + { + # radial distances change when the mixed subtrees are processed because + # the root of the subtree is considered instead of the tree root + # heterogeneous forks are not valid forking points + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1.0, 1.4142135, 2.828427, 1.0], + "expected_with_subtrees": [1.0, 1.4142135, 1.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1.0, 1.4142135, 2.828427], + "expected_with_subtrees": [1.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.4142135,], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.], + "expected_with_subtrees": [1.], + } + ], "section_end_distances": [ { "kwargs": {"neurite_type": NeuriteType.all}, @@ -785,7 +895,7 @@ def _neurite_features(mode): }, { "kwargs": {"neurite_type": NeuriteType.axon}, - "expected_wout_subtrees": 0., + "expected_wout_subtrees": [], "expected_with_subtrees": [1.414214, 1., 1.], }, { @@ -807,7 +917,7 @@ def _neurite_features(mode): }, { "kwargs": {"neurite_type": NeuriteType.axon}, - "expected_wout_subtrees": 0., + "expected_wout_subtrees": [], "expected_with_subtrees": [1., 1.], }, { @@ -816,6 +926,138 @@ def _neurite_features(mode): "expected_with_subtrees": [1., 1.], } ], + "section_taper_rates": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0.0] * 11, + "expected_with_subtrees": [0.0] * 11, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [0.0] * 8, + "expected_with_subtrees": [0.0] * 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.0] * 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.0] * 3, + "expected_with_subtrees": [0.0] * 3, + } + ], + "section_bif_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1., 1.414214, 1.414214, 1.], + "expected_with_subtrees": [1., 1.414214, 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1., 1.414214, 1.414214], + "expected_with_subtrees": [1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.414214], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.], + "expected_with_subtrees": [1.], + }, + ], + "section_branch_orders": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0, 1, 1, 0, 1, 1, 2, 2, 0, 1, 1], + "expected_with_subtrees": [0, 1, 1, 0, 1, 1, 2, 2, 0, 1, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [0, 1, 1, 0, 1, 1, 2, 2], + "expected_with_subtrees": [0, 1, 1, 0, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1, 2, 2], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0, 1, 1], + "expected_with_subtrees": [0, 1, 1], + }, + ], + "section_bif_branch_orders": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0, 0, 1, 0], + "expected_with_subtrees": [0, 1, 0], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [0, 0, 1], + "expected_with_subtrees": [0], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0], + "expected_with_subtrees": [0], + }, + ], + "section_term_branch_orders": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1, 1, 1, 2, 2, 1, 1], + "expected_with_subtrees": [1, 1, 1, 2, 2, 1, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1, 1, 1, 2, 2], + "expected_with_subtrees": [1, 1, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [2, 2], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1, 1], + "expected_with_subtrees": [1, 1], + }, + ], + "section_strahler_orders": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [2, 1, 1, 2, 1, 2, 1, 1, 2, 1, 1], + "expected_with_subtrees": [2, 1, 1, 2, 1, 2, 1, 1, 2, 1, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [2, 1, 1, 2, 1, 2, 1, 1], + "expected_with_subtrees": [2, 1, 1, 2, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [2, 1, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [2, 1, 1], + "expected_with_subtrees": [2, 1, 1], + }, + ], "segment_lengths": [ { "kwargs": {"neurite_type": NeuriteType.all}, @@ -833,7 +1075,7 @@ def _neurite_features(mode): }, { "kwargs": {"neurite_type": NeuriteType.axon}, - "expected_wout_subtrees": 0., + "expected_wout_subtrees": [], "expected_with_subtrees": [1.414214, 1., 1.], }, { @@ -944,6 +1186,112 @@ def _neurite_features(mode): "expected_with_subtrees": [0.0] * 3, }, ], + "segment_radial_distances": [ + { + # radial distances change when the mixed subtrees are processed because + # the root of the subtree is considered + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [0.5, 1.5, 1.118034, 0.70710677, 2.236068, 2.1213202, 3.2015622, 3.2015622, + 0.5, 1.5, 1.118034], + "expected_with_subtrees": + [0.5, 1.5, 1.118034, 0.70710677, 2.236068, 0.707107, 1.802776, 1.802776, + 0.5, 1.5, 1.118034], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": + [0.5, 1.5, 1.118034, 0.70710677, 2.236068, 2.1213202, 3.2015622, 3.2015622], + "expected_with_subtrees": + [0.5, 1.5, 1.118034, 0.70710677, 2.236068], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.707107, 1.802776, 1.802776], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.5, 1.5, 1.118034], + "expected_with_subtrees": [0.5, 1.5, 1.118034], + }, + ], + "segment_midpoints": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [ + [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-2.0, 0.5, 0.0], [0.5, 1.5, 0. ], + [1.0, 3.0, 0.0], [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0], + [0.0, -1.5, 0.0], [0., -2.5, 0.0], [0.5, -2.0, 0.0]], + "expected_with_subtrees": [ + [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-2.0, 0.5, 0.0], [0.5, 1.5, 0. ], + [1.0, 3.0, 0.0], [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0], + [0.0, -1.5, 0.0], [0., -2.5, 0.0], [0.5, -2.0, 0.0]], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [ + [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-2.0, 0.5, 0.0], [0.5, 1.5, 0. ], + [1.0, 3.0, 0.0], [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0]], + "expected_with_subtrees": [ + [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-2.0, 0.5, 0.0], [0.5, 1.5, 0. ], + [1.0, 3.0, 0.0]], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [[1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0]], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [[0.0, -1.5, 0.0], [0., -2.5, 0.0], [0.5, -2.0, 0.0]], + "expected_with_subtrees": [[0.0, -1.5, 0.0], [0., -2.5, 0.0], [0.5, -2.0, 0.0]], + }, + ], + "segment_meander_angles": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [], + }, + ], + "number_of_sections": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 11, + "expected_with_subtrees": 11, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 8, + "expected_with_subtrees": 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0, + "expected_with_subtrees": 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 3, + "expected_with_subtrees": 3, + }, + ], "number_of_bifurcations": [ { "kwargs": {"neurite_type": NeuriteType.all}, @@ -988,36 +1336,170 @@ def _neurite_features(mode): "expected_with_subtrees": 1, }, ], - "section_bif_lengths": [ + "volume_density": [ # neurites are flat :( { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [1., 1.414214, 1.414214, 1.], - "expected_with_subtrees": [1., 1.414214, 1.], + "expected_wout_subtrees": np.nan, + "expected_with_subtrees": np.nan, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [1., 1.414214, 1.414214], - "expected_with_subtrees": [1.], + "expected_wout_subtrees": np.nan, + "expected_with_subtrees": np.nan, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0, + "expected_with_subtrees": np.nan, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": np.nan, + "expected_with_subtrees": np.nan, + }, + ], + "local_bifurcation_angles": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0.5 * np.pi, 0.785398, 0.5 * np.pi, 0.5 * np.pi], + "expected_with_subtrees": [0.5 * np.pi, 0.5 * np.pi, 0.5 * np.pi], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [0.5 * np.pi, 0.785398, 0.5 * np.pi], + "expected_with_subtrees": [1.570796], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.414214], + "expected_with_subtrees": [0.5 * np.pi], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1.], - "expected_with_subtrees": [1.], - } + "expected_wout_subtrees": [0.5 * np.pi], + "expected_with_subtrees": [0.5 * np.pi], + }, + ], + "remote_bifurcation_angles": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0.5 * np.pi, 0.785398, 0.5 * np.pi, 0.5 * np.pi], + "expected_with_subtrees": [0.5 * np.pi, 0.5 * np.pi, 0.5 * np.pi], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [0.5 * np.pi, 0.785398, 0.5 * np.pi], + "expected_with_subtrees": [1.570796], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.5 * np.pi], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.5 * np.pi], + "expected_with_subtrees": [0.5 * np.pi], + }, + ], + "sibling_ratios": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1.0] * 4, + "expected_with_subtrees": [1.0] * 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1.0] * 3, + "expected_with_subtrees": [1.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.0], + "expected_with_subtrees": [1.0], + }, + ], + "partition_pairs": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [(1.0, 1.0), (1.0, 3.0), (1.0, 1.0), (1.0, 1.0)], + "expected_with_subtrees": [(1.0, 1.0), (1.0, 1.0), (1.0, 1.0)], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [(1.0, 1.0), (1.0, 3.0), (1.0, 1.0)], + "expected_with_subtrees": [(1.0, 1.0)], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [(1.0, 1.0)], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [(1.0, 1.0)], + "expected_with_subtrees": [(1.0, 1.0)], + }, + ], + "diameter_power_relations": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [2.0] * 4, + "expected_with_subtrees": [2.0] * 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [2.0] * 3, + "expected_with_subtrees": [2.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [2.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [2.0], + "expected_with_subtrees": [2.0], + }, + ], + "bifurcation_partitions": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1.0, 3.0, 1.0, 1.0], + "expected_with_subtrees": [1.0] * 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1.0, 3.0, 1.0], + "expected_with_subtrees": [1.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.0], + "expected_with_subtrees": [1.0], + }, ], } - features_not_tested = set(_NEURITE_FEATURES) - set(features.keys()) + features_not_tested = list( + set(_NEURITE_FEATURES) - set(features.keys()) - set(_MORPHOLOGY_FEATURES) + ) - #assert not features_not_tested, ( - # "The following morphology tests need to be included in the mixed morphology tests:\n" - # f"{features_not_tested}" - #) +# assert not features_not_tested, ( +# "The following morphology tests need to be included in the tests:\n\n" + +# "\n".join(sorted(features_not_tested)) + "\n" +# ) return _dispatch_features(features, mode) @@ -1026,27 +1508,11 @@ def _neurite_features(mode): "feature_name, kwargs, expected", _neurite_features(mode="wout-subtrees") ) def test_morphology__neurite_features_wout_subtrees(feature_name, kwargs, expected, mixed_morph): - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - npt.assert_allclose( - get(feature_name, mixed_morph, **kwargs), - expected, - atol=1e-6 - ) + _assert_feature_equal(mixed_morph, feature_name, expected, kwargs, use_subtrees=False) @pytest.mark.parametrize( "feature_name, kwargs, expected", _neurite_features(mode="with-subtrees") ) def test_morphology__neurite_features_with_subtrees(feature_name, kwargs, expected, mixed_morph): - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - npt.assert_allclose( - get(feature_name, mixed_morph, use_subtrees=True, **kwargs), - expected, - atol=1e-6 - ) + _assert_feature_equal(mixed_morph, feature_name, expected, kwargs, use_subtrees=True) From 640fd7016fd9aac050a8de428d2972a572b2e5ab Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 3 Mar 2022 18:30:20 +0100 Subject: [PATCH 51/87] Handle section path lengths --- neurom/core/morphology.py | 11 ++++++--- neurom/features/morphology.py | 2 ++ neurom/features/neurite.py | 33 +++++++++++++++++-------- tests/test_mixed.py | 46 +++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index ab4c5797..376747a8 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -76,7 +76,7 @@ def append_section(self, section): return self.morphio_section.append_section(section) def is_homogeneous_point(self): - """A section is homogeneous if it has the same type with its children""" + """A section is homogeneous if it has the same type with its children.""" return all(c.type == self.type for c in self.children) def is_forking_point(self): @@ -216,9 +216,11 @@ def __repr__(self): def _homogeneous_subtrees(neurite): - """Returns a dictionary the keys of which are section types and the values are the - sub-neurites. A sub-neurite can be either the entire tree or a homogeneous downstream + """Returns a dictionary the keys of which are section types and the values are the sub-neurites. + + A sub-neurite can be either the entire tree or a homogeneous downstream sub-tree. + Note: Only two different mixed types are allowed """ homogeneous_neurites = {neurite.root_node.type: neurite} @@ -342,6 +344,7 @@ def iter_segments( 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 + section_filter: optional section level filter Note: This is a convenience function provided for generic access to @@ -439,7 +442,7 @@ def volume(self): return total_volume(self) def is_heterogeneous(self) -> bool: - """Returns true if the neurite consists of more that one section types""" + """Returns true if the neurite consists of more that one section types.""" return self.morphio_root_node.is_heterogeneous() def iter_sections(self, order=Section.ipreorder, neurite_order=NeuriteIter.FileOrder): diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index f51e5a3f..a7bdf76b 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -484,6 +484,7 @@ def trunk_mean_radius(neurite): "values excluded all the points of the section so the radius of the first " "point after the 'min_length_filter' path distance is returned." ) + # pylint: disable = invalid-unary-operand-type return points[~valid_max, COLS.R][0] return points[valid_pts, COLS.R].mean() @@ -605,6 +606,7 @@ def sholl_frequency( 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``. + use_subtrees: Enable mixed subtree processing Note: Given a morphology, the soma center is used for the concentric circles, diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index efb63c8d..29819197 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project + # Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project # All rights reserved. # # This file is part of NeuroM @@ -63,14 +63,16 @@ def _map_sections(fun, neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Map `fun` to all the sections.""" - check_type = is_type(section_type) + def homogeneous_filter(section): + return check_type(section) and Section.is_homogeneous_point(section) + if ( iterator_type in (Section.ibifurcation_point, Section.iforking_point) and section_type != NeuriteType.all ): - filt = lambda s: check_type(s) and Section.is_homogeneous_point(s) + filt = homogeneous_filter else: filt = check_type @@ -89,14 +91,17 @@ def max_radial_distance(neurite, origin=None, section_type=NeuriteType.all): @feature(shape=()) def number_of_segments(neurite, section_type=NeuriteType.all): """Number of segments.""" - count_segments = lambda s: len(s.points) - 1 + def count_segments(section): + return len(section.points) - 1 return sum(_map_sections(count_segments, neurite, section_type=section_type)) @feature(shape=()) def number_of_sections(neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Number of sections. For a morphology it will be a sum of all neurites sections numbers.""" - return len(_map_sections(lambda s: s, neurite, iterator_type=iterator_type, section_type=section_type)) + return len( + _map_sections(lambda s: s, neurite, iterator_type=iterator_type, section_type=section_type) + ) @feature(shape=()) @@ -187,14 +192,22 @@ def section_term_branch_orders(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) -def section_path_distances(neurite): +def section_path_distances(neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Path lengths.""" + def takeuntil(predicate, iterable): + """Similar to itertools.takewhile but it returns the last element before stopping.""" + for x in iterable: + yield x + if predicate(x): + break + def pl2(node): """Calculate the path length using cached section lengths.""" - return sum(n.length for n in node.iupstream()) + sections = takeuntil(lambda s: s.id == neurite.root_node.id, node.iupstream()) + return sum(n.length for n in sections) - return _map_sections(pl2, neurite) + return _map_sections(pl2, neurite, iterator_type=iterator_type, section_type=section_type) ################################################################################ @@ -445,9 +458,9 @@ def section_bif_radial_distances(neurite, origin=None, section_type=NeuriteType. @feature(shape=(...,)) -def terminal_path_lengths(neurite): +def terminal_path_lengths(neurite, section_type=NeuriteType.all): """Get the path lengths to each terminal point.""" - return _map_sections(sf.section_path_length, neurite, Section.ileaf) + return section_path_distances(neurite, iterator_type=Section.ileaf, section_type=section_type) @feature(shape=()) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 3c7b8a65..04eadb86 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1490,6 +1490,52 @@ def _neurite_features(mode): "expected_with_subtrees": [1.0], }, ], + "section_path_distances": [ + { + # subtree path distances are calculated to the root of the subtree + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [ + 1.0, 2.0, 2.0, 1.414213, 3.414216, 2.828427, 3.828427, 3.828427, 1.0, 2.0, 2.0 + ], + "expected_with_subtrees": [ + 1.0, 2.0, 2.0, 1.414213, 3.414216, 1.414213, 2.414213, 2.414213, 1.0, 2.0, 2.0 + ] + + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [ + 1.0, 2.0, 2.0, 1.414213, 3.414216, 2.828427, 3.828427, 3.828427], + "expected_with_subtrees": [1.0, 2.0, 2.0, 1.414213, 3.414216], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.414213, 2.414213, 2.414213], + }, + ], + "terminal_path_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [2.0, 2.0, 3.414213, 3.828427, 3.828427, 2.0, 2.0], + "expected_with_subtrees": [2.0, 2.0, 3.414213, 2.414213, 2.414213, 2.0, 2.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [2.0, 2.0, 3.414213, 3.828427, 3.828427], + "expected_with_subtrees": [2.0, 2.0, 3.414213], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [2.414213, 2.414213], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [2.0, 2.0], + "expected_with_subtrees": [2.0, 2.0], + }, + ], } features_not_tested = list( From 3261c0ffbfaec5f3891ae9c42c1a8241d4ede81b Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 3 Mar 2022 21:05:01 +0100 Subject: [PATCH 52/87] Remove check --- neurom/core/morphology.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 376747a8..cb49551e 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -227,9 +227,6 @@ def _homogeneous_subtrees(neurite): for section in neurite.root_node.ipreorder(): if section.type not in homogeneous_neurites: homogeneous_neurites[section.type] = Neurite(section.morphio_section) - if len(homogeneous_neurites) != 2: - raise TypeError( - f"Subtree types must be exactly two. Found {len(homogeneous_neurites)} instead.") return list(homogeneous_neurites.values()) From 7d6607561da12b6715124474788cb064ee571b12 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 3 Mar 2022 21:05:32 +0100 Subject: [PATCH 53/87] Add test for section_path_length --- tests/features/test_section.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/features/test_section.py b/tests/features/test_section.py index d549410a..8d1f478e 100644 --- a/tests/features/test_section.py +++ b/tests/features/test_section.py @@ -37,7 +37,6 @@ import pytest import numpy as np from numpy import testing as npt -from mock import Mock from neurom import load_morphology, iter_sections from neurom import morphmath @@ -74,6 +73,21 @@ def test_segment_taper_rates(): sec = Mock(points=np.array([[0., 0., 0., 2.], [1., 0., 0., 1.], [2., 0., 0., 0.]])) npt.assert_almost_equal(section.segment_taper_rates(sec), [-2., -2.]) +def test_section_path_length(): + m = load_morphology( + """ + 1 1 0 0 0 0.5 -1 + 2 3 1 0 0 0.1 1 + 3 3 2 0 0 0.1 2 + 4 3 3 0 0 0.1 3 + 5 3 2 1 0 0.1 3 + """, + reader="swc", + ) + + sec = m.sections[1] + npt.assert_almost_equal(section.section_path_length(sec), 2.0) + def test_section_area(): sec = load_morphology(StringIO(u"""((CellBody) (0 0 0 2)) From 8112a7aba4b5a6d4c03228f2ac3d4cfed21b92bf Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 4 Mar 2022 00:16:07 +0100 Subject: [PATCH 54/87] Simplify volume density --- neurom/features/neurite.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 29819197..68e9c523 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -49,7 +49,7 @@ import numpy as np from neurom import morphmath from neurom.core.types import NeuriteType -from neurom.core.morphology import Section, iter_sections +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.morphmath import convex_hull @@ -475,22 +475,18 @@ def volume_density(neurite, section_type=NeuriteType.all): .. note:: Returns `np.nan` if the convex hull computation fails. """ - try: + neurite_volume = total_volume(neurite, section_type=section_type) + + def get_points(section): + return section.points[:, COLS.XYZ].tolist() - if section_type != NeuriteType.all: + points = list( + chain.from_iterable(_map_sections(get_points, neurite, section_type=section_type)) + ) - sections = list(iter_sections(neurite, section_filter=is_type(section_type))) - points = [ - point - for section in sections - for point in section.points[:, COLS.XYZ] - ] - volume = convex_hull(points).volume - neurite_volume = sum(s.volume for s in sections) + try: - else: - volume = convex_hull(neurite).volume - neurite_volume = neurite.volume + volume = convex_hull(points).volume except scipy.spatial.qhull.QhullError: L.exception('Failure to compute neurite volume using the convex hull. ' From d80e42f7824aec9f5b276066510bef5e2759bd13 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 4 Mar 2022 11:35:24 +0100 Subject: [PATCH 55/87] Adapt principal_direction_extents --- neurom/features/neurite.py | 13 +++++++++++-- tests/test_mixed.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 68e9c523..f7cbab68 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -480,6 +480,7 @@ def volume_density(neurite, section_type=NeuriteType.all): def get_points(section): return section.points[:, COLS.XYZ].tolist() + # note: duplicate points included but not affect the convex hull calculation points = list( chain.from_iterable(_map_sections(get_points, neurite, section_type=section_type)) ) @@ -521,14 +522,22 @@ def section_end_distances(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) -def principal_direction_extents(neurite, direction=0): +def principal_direction_extents(neurite, direction=0, section_type=NeuriteType.all): """Principal direction extent of neurites in morphologies. Note: Principal direction extents are always sorted in descending order. Therefore, by default the maximal principal direction extent is returned. """ - return [morphmath.principal_direction_extent(neurite.points[:, COLS.XYZ])[direction]] + def get_points(section): + return section.points[:, COLS.XYZ].tolist() + + # Note: duplicate points are included and need to be removed + points = list( + chain.from_iterable(_map_sections(get_points, neurite, section_type=section_type)) + ) + + return [morphmath.principal_direction_extent(np.unique(points, axis=0))[direction]] @feature(shape=(...,)) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 04eadb86..c37d16a8 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1536,16 +1536,39 @@ def _neurite_features(mode): "expected_with_subtrees": [2.0, 2.0], }, ], + "principal_direction_extents": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [2.25, 3.395976, 2.0], + "expected_with_subtrees": [2.25, 2.359056, 2.828427, 2.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [2.25, 3.395976], + "expected_with_subtrees": [2.25, 2.359056], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [2.828427], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [2.0], + "expected_with_subtrees": [2.0], + }, + ] + } features_not_tested = list( set(_NEURITE_FEATURES) - set(features.keys()) - set(_MORPHOLOGY_FEATURES) ) -# assert not features_not_tested, ( -# "The following morphology tests need to be included in the tests:\n\n" + -# "\n".join(sorted(features_not_tested)) + "\n" -# ) + assert not features_not_tested, ( + "The following morphology tests need to be included in the tests:\n\n" + + "\n".join(sorted(features_not_tested)) + "\n" + ) return _dispatch_features(features, mode) From f8666beb44c0c5cf732d989ab39f1e5d912dafb6 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 9 Mar 2022 17:07:32 +0100 Subject: [PATCH 56/87] Converted to more complex morph and converted branch-order partition_asymmetry --- neurom/features/neurite.py | 23 +- tests/test_mixed.py | 964 ++++++++++++++++++++++--------------- 2 files changed, 585 insertions(+), 402 deletions(-) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index f7cbab68..9155a4b9 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -334,7 +334,9 @@ def remote_bifurcation_angles(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) -def partition_asymmetry(neurite, variant='branch-order', method='petilla'): +def partition_asymmetry( + neurite, variant='branch-order', method='petilla', section_type=NeuriteType.all +): """Partition asymmetry at bf points. Variant: length is a different definition, as the absolute difference in @@ -350,10 +352,25 @@ def partition_asymmetry(neurite, variant='branch-order', method='petilla'): 'either "petilla" or "uylings"') if variant == 'branch-order': + + def it_type(section): + + if section_type == NeuriteType.all: + return Section.ipreorder(section) + + check = is_type(section_type) + return (s for s in section.ipreorder() if check(s)) + + function = partial( + bf.partition_asymmetry, uylings=method == 'uylings', iterator_type=it_type + ) + return _map_sections( - partial(bf.partition_asymmetry, uylings=method == 'uylings'), + function, neurite, - Section.ibifurcation_point) + iterator_type=Section.ibifurcation_point, + section_type=section_type + ) asymmetries = [] neurite_length = total_length(neurite) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index c37d16a8..f362004b 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1,3 +1,4 @@ +import sys import warnings import pytest import neurom @@ -8,6 +9,7 @@ from neurom.features import _MORPHOLOGY_FEATURES, _NEURITE_FEATURES import collections.abc + @pytest.fixture def mixed_morph(): """ @@ -17,24 +19,33 @@ def mixed_morph(): """ return neurom.load_morphology( """ - 1 1 0 0 0 0.5 -1 - 2 3 -1 0 0 0.1 1 - 3 3 -2 0 0 0.1 2 - 4 3 -3 0 0 0.1 3 - 5 3 -2 1 0 0.1 3 - 6 3 0 1 0 0.1 1 - 7 3 1 2 0 0.1 6 - 8 3 1 4 0 0.1 7 - 9 2 2 3 0 0.1 7 - 10 2 2 4 0 0.1 9 - 11 2 3 3 0 0.1 9 - 12 4 0 -1 0 0.1 1 - 13 4 0 -2 0 0.1 12 - 14 4 0 -3 0 0.1 13 - 15 4 1 -2 0 0.1 13 + 1 1 0 0 0 0.5 -1 + 2 3 -1 0 0 0.1 1 + 3 3 -2 0 0 0.1 2 + 4 3 -3 0 0 0.1 3 + 5 3 -3 0 1 0.1 4 + 6 3 -3 0 -1 0.1 4 + 7 3 -2 1 0 0.1 3 + 8 3 0 1 0 0.1 1 + 9 3 1 2 0 0.1 8 + 10 3 1 4 0 0.1 9 + 11 3 1 4 1 0.1 10 + 12 3 1 4 -1 0.1 10 + 13 2 2 3 0 0.1 9 + 14 2 2 4 0 0.1 13 + 15 2 3 3 0 0.1 13 + 16 2 3 3 1 0.1 15 + 17 2 3 3 -1 0.1 15 + 18 4 0 -1 0 0.1 1 + 19 4 0 -2 0 0.1 18 + 20 4 0 -3 0 0.1 19 + 21 4 0 -3 1 0.1 20 + 22 4 0 -3 -1 0.1 20 + 23 4 1 -2 0 0.1 19 """, reader="swc") + def _assert_feature_equal(obj, feature_name, expected_values, kwargs, use_subtrees): def innermost_value(iterable): @@ -55,7 +66,6 @@ def innermost_value(iterable): warnings.simplefilter("ignore") values = get(feature_name, obj, use_subtrees=use_subtrees, **kwargs) - # handle empty lists because allclose always passes in that case. # See: https://github.com/numpy/numpy/issues/11071 if isinstance(values, collections.abc.Iterable): @@ -77,8 +87,8 @@ def innermost_value(iterable): def _dispatch_features(features, mode): - for feature_name, configurations in features.items(): + for cfg in configurations: kwargs = cfg["kwargs"] if "kwargs" in cfg else {} @@ -116,23 +126,23 @@ def _morphology_features(mode): "number_of_sections_per_neurite": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [3, 5, 3], - "expected_with_subtrees": [3, 2, 3, 3], + "expected_wout_subtrees": [5, 9, 5], + "expected_with_subtrees": [5, 4, 5, 5], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [3, 5], - "expected_with_subtrees": [3, 2], + "expected_wout_subtrees": [5, 9], + "expected_with_subtrees": [5, 4], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [3], + "expected_with_subtrees": [5], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [3], - "expected_with_subtrees": [3], + "expected_wout_subtrees": [5], + "expected_with_subtrees": [5], } ], "max_radial_distance": [ @@ -141,8 +151,8 @@ def _morphology_features(mode): # with subtrees AoD subtrees are considered separately and the distance is calculated # from their respective roots. [1, 4] is the furthest point in this case "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 3.60555127546398, - "expected_with_subtrees": 3.16227766016837, + "expected_wout_subtrees": 3.741657, + "expected_with_subtrees": 3.316625, }, { # with a global origin, AoD axon subtree [2, 4] is always furthest from soma @@ -152,90 +162,92 @@ def _morphology_features(mode): }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 3.60555127546398, # [3, 3] - [0, 1] - "expected_with_subtrees": 3.16227766016837, # [1, 4] - [0, 1] + "expected_wout_subtrees": 3.741657, + "expected_with_subtrees": 3.316625, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite, "origin": np.array([0., 0., 0.])}, - "expected_wout_subtrees": 4.47213595499958, # [2, 4] - [0, 0] - "expected_with_subtrees": 4.12310562561766, # [1, 4] - [0, 0] + "expected_wout_subtrees": 4.472136, + "expected_with_subtrees": 4.242641, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0.0, - "expected_with_subtrees": 2.23606797749979, # [3, 3] - [1, 2] + "expected_with_subtrees": 2.44949, }, { "kwargs": {"neurite_type": NeuriteType.axon, "origin": np.array([0., 0., 0.])}, "expected_wout_subtrees": 0.0, - "expected_with_subtrees": 4.47213595499958, # [2, 4] - [0, 0] + "expected_with_subtrees": 4.47213595499958, } ], "total_length_per_neurite": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [3., 6.82842712474619, 3.], - "expected_with_subtrees": [3., 3.414213562373095, 3.414213562373095, 3], + "expected_wout_subtrees": [5., 10.828427, 5.], + "expected_with_subtrees": [5., 5.414213, 5.414213, 5.], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [3., 6.82842712474619], - "expected_with_subtrees": [3., 3.414213562373095], + "expected_wout_subtrees": [5., 10.828427], + "expected_with_subtrees": [5., 5.414214], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [3.414213562373095], + "expected_with_subtrees": [5.414214], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [3.], - "expected_with_subtrees": [3.], + "expected_wout_subtrees": [5.], + "expected_with_subtrees": [5.], } ], "total_area_per_neurite" : [ { + # total length x 2piR "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [1.884956, 4.290427, 1.884956], # total_length * 2piR - "expected_with_subtrees": [1.884956, 2.145214, 2.145214, 1.884956], + "expected_wout_subtrees": [3.141593, 6.803702, 3.141593], + "expected_with_subtrees": [3.141593, 3.401851, 3.401851, 3.141593], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [1.884956, 4.290427], - "expected_with_subtrees": [1.884956, 2.145214], + "expected_wout_subtrees": [3.141593, 6.803702], + "expected_with_subtrees": [3.141593, 3.401851], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [2.145214], + "expected_with_subtrees": [3.401851], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1.884956], - "expected_with_subtrees": [1.884956], + "expected_wout_subtrees": [3.141593], + "expected_with_subtrees": [3.141593], } ], "total_volume_per_neurite": [ + # total_length * piR^2 { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [0.09424778, 0.21452136, 0.09424778], # total_length * piR^2 - "expected_with_subtrees": [0.09424778, 0.10726068, 0.10726068, 0.09424778], + "expected_wout_subtrees": [0.15708 , 0.340185, 0.15708 ], + "expected_with_subtrees": [0.15708 , 0.170093, 0.170093, 0.15708], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [0.09424778, 0.21452136], - "expected_with_subtrees": [0.09424778, 0.10726068], + "expected_wout_subtrees": [0.15708 , 0.340185], + "expected_with_subtrees": [0.15708 , 0.170093], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.10726068], + "expected_with_subtrees": [0.170093], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.09424778], - "expected_with_subtrees": [0.09424778], + "expected_wout_subtrees": [0.15708], + "expected_with_subtrees": [0.15708], } ], "trunk_origin_azimuths": [ # Not applicable to distal subtrees @@ -254,6 +266,11 @@ def _morphology_features(mode): "expected_wout_subtrees": [], "expected_with_subtrees": [], }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.0], + "expected_with_subtrees": [0.0], + }, ], "trunk_origin_elevations": [ # Not applicable to distal subtrees { @@ -271,6 +288,11 @@ def _morphology_features(mode): "expected_wout_subtrees": [], "expected_with_subtrees": [], }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [-1.570796], + "expected_with_subtrees": [-1.570796], + }, ], "trunk_vectors": [ { @@ -311,6 +333,11 @@ def _morphology_features(mode): "expected_wout_subtrees": [], "expected_with_subtrees": [], }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.], + "expected_with_subtrees": [0.], + }, ], "trunk_angles_from_vector": [ { @@ -418,26 +445,26 @@ def _morphology_features(mode): "expected_with_subtrees": 1, }, ], - "neurite_volume_density": [ # our morphology is flat :( + "neurite_volume_density": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [np.nan, np.nan, np.nan], - "expected_with_subtrees": [np.nan, np.nan, np.nan, np.nan], + "expected_wout_subtrees": [0.235619, 0.063785, 0.235619], + "expected_with_subtrees": [0.235619, 0.255139, 0.170093, 0.235619], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [np.nan, np.nan], - "expected_with_subtrees": [np.nan, np.nan], + "expected_wout_subtrees": [0.235619, 0.063785], + "expected_with_subtrees": [0.235619, 0.255139], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [np.nan], + "expected_with_subtrees": [0.170093], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [np.nan], - "expected_with_subtrees": [np.nan], + "expected_wout_subtrees": [0.235619], + "expected_with_subtrees": [0.235619], }, ], "sholl_crossings": [ @@ -473,12 +500,11 @@ def _morphology_features(mode): "expected_wout_subtrees": [0, 2], "expected_with_subtrees": [0, 1], }, -# { see #987 -# "kwargs": {"neurite_type": NeuriteType.axon}, -# "kwargs": {"step_size": 3}, -# "expected_wout_subtrees": [0, 0], -# "expected_with_subtrees": [0, 1], -# }, + { + "kwargs": {"neurite_type": NeuriteType.axon, "step_size": 3}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0, 1], + }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite, "step_size": 2}, "expected_wout_subtrees": [0, 1], @@ -533,23 +559,23 @@ def _morphology_features(mode): "total_depth": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 0.0, - "expected_with_subtrees": 0.0, + "expected_wout_subtrees": 2.0, + "expected_with_subtrees": 2.0, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 0.0, - "expected_with_subtrees": 0.0, + "expected_wout_subtrees": 2.0, + "expected_with_subtrees": 2.0, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0.0, - "expected_with_subtrees": 0.0, + "expected_with_subtrees": 2.0, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": 0.0, - "expected_with_subtrees": 0.0, + "expected_wout_subtrees": 2.0, + "expected_with_subtrees": 2.0, }, ], } @@ -582,166 +608,167 @@ def _neurite_features(mode): "number_of_segments": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 11, - "expected_with_subtrees": 11, + "expected_wout_subtrees": 19, + "expected_with_subtrees": 19, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 8, - "expected_with_subtrees": 5, + "expected_wout_subtrees": 14, + "expected_with_subtrees": 9, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0, - "expected_with_subtrees": 3, + "expected_with_subtrees": 5, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": 3, - "expected_with_subtrees": 3, + "expected_wout_subtrees": 5, + "expected_with_subtrees": 5, }, ], "number_of_leaves": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 7, - "expected_with_subtrees": 7, + "expected_wout_subtrees": 11, + "expected_with_subtrees": 11, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 5, - "expected_with_subtrees": 3, + "expected_wout_subtrees": 8, + "expected_with_subtrees": 5, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0, - "expected_with_subtrees": 2, + "expected_with_subtrees": 3, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": 2, - "expected_with_subtrees": 2, + "expected_wout_subtrees": 3, + "expected_with_subtrees": 3, }, ], "total_length": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 12.828427, - "expected_with_subtrees": 12.828427, + "expected_wout_subtrees": 20.828427, + "expected_with_subtrees": 20.828427, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 9.828427, - "expected_with_subtrees": 6.414214, + "expected_wout_subtrees": 15.828427, + "expected_with_subtrees": 10.414214, }, { "kwargs": {"neurite_type": NeuriteType.axon}, - "expected_wout_subtrees": 0., - "expected_with_subtrees": 3.414214, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 5.414214, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": 3., - "expected_with_subtrees": 3., + "expected_wout_subtrees": 5., + "expected_with_subtrees": 5., } ], "total_area": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 8.060339, - "expected_with_subtrees": 8.060339, + "expected_wout_subtrees": 13.086887, + "expected_with_subtrees": 13.086887, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 6.175383, - "expected_with_subtrees": 4.030170, + "expected_wout_subtrees": 9.945294, + "expected_with_subtrees": 6.543443, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0., - "expected_with_subtrees": 2.145214, + "expected_with_subtrees": 3.401851, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": 1.884956, - "expected_with_subtrees": 1.884956, + "expected_wout_subtrees": 3.141593, + "expected_with_subtrees": 3.141593, } ], "total_volume": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 0.403016, - "expected_with_subtrees": 0.403016, + "expected_wout_subtrees": 0.654344, + "expected_with_subtrees": 0.654344, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 0.308769, - "expected_with_subtrees": 0.201508, + "expected_wout_subtrees": 0.497265, + "expected_with_subtrees": 0.327172, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0., - "expected_with_subtrees": 0.107261, + "expected_with_subtrees": 0.170093, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": 0.0942478, - "expected_with_subtrees": 0.0942478, + "expected_wout_subtrees": 0.15708, + "expected_with_subtrees": 0.15708, } ], "section_lengths": [ { "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + [1.] * 5 + [1.414214, 2., 1., 1.] + [1.414214, 1., 1., 1., 1.] + [1.] * 5, "expected_with_subtrees": - [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + [1.] * 5 + [1.414214, 2., 1., 1.] + [1.414214, 1., 1., 1., 1.] + [1.] * 5, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": - [1., 1., 1., 1.414214, 2., 1.414214, 1., 1.], + [1.] * 5 + [1.414214, 2., 1., 1.] + [1.414214, 1., 1., 1., 1], "expected_with_subtrees": - [1., 1., 1., 1.414214, 2.], + [1.] * 5 + [1.414214, 2., 1., 1.], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.414214, 1., 1.], + "expected_with_subtrees": [1.414214, 1., 1., 1., 1.], + }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1., 1., 1.], - "expected_with_subtrees": [1., 1., 1.], + "expected_wout_subtrees": [1.] * 5, + "expected_with_subtrees": [1.] * 5, } ], "section_areas": [ { "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, - 0.628318, 0.628318, 0.628318, 0.628318, 0.628318], + [0.628318] * 5 + [0.888577, 1.256637, 0.628319, 0.628319] + + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319] + [0.628318] * 5, "expected_with_subtrees": - [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, - 0.628318, 0.628318, 0.628318, 0.628318, 0.628318], + [0.628318] * 5 + [0.888577, 1.256637, 0.628319, 0.628319] + + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319] + [0.628318] * 5, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": - [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, - 0.628318, 0.628318], + [0.628318] * 5 + [0.888577, 1.256637, 0.628319, 0.628319] + + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319], "expected_with_subtrees": - [0.628318, 0.628319, 0.628319, 0.888577, 1.256637], + [0.628318] * 5 + [0.888577, 1.256637, 0.628319, 0.628319], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.888576, 0.628318, 0.628318], + "expected_with_subtrees": [0.888577, 0.628319, 0.628319, 0.628319, 0.628319], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.628318, 0.628318, 0.628318], - "expected_with_subtrees": [0.628318, 0.628318, 0.628318], + "expected_wout_subtrees": [0.628318] * 5, + "expected_with_subtrees": [0.628318] * 5, } ], @@ -749,51 +776,53 @@ def _neurite_features(mode): { "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, - 0.031415, 0.031415, 0.031415, 0.031415], + [0.031416] * 5 + [0.044429, 0.062832, 0.031416, 0.031416] + + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416] + + [0.031416] * 5, "expected_with_subtrees": - [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, - 0.031415, 0.031415, 0.031415, 0.031415], + [0.031416] * 5 + [0.044429, 0.062832, 0.031416, 0.031416] + + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416] + + [0.031416] * 5, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": - [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, - 0.031415], + [0.031416] * 5 + [0.044429, 0.062832, 0.031416, 0.031416] + + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416], "expected_with_subtrees": - [0.031415, 0.031415, 0.031415, 0.044428, 0.062831], + [0.031416] * 5 + [0.044429, 0.062832, 0.031416, 0.031416], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.044428, 0.031415, 0.031415], + "expected_with_subtrees": [0.044429, 0.031416, 0.031416, 0.031416, 0.031416], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.031415, 0.031415, 0.031415], - "expected_with_subtrees": [0.031415, 0.031415, 0.031415], + "expected_wout_subtrees": [0.031415] * 5, + "expected_with_subtrees": [0.031415] * 5, } ], "section_tortuosity": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [1.0] * 11, - "expected_with_subtrees": [1.0] * 11, + "expected_wout_subtrees": [1.0] * 19, + "expected_with_subtrees": [1.0] * 19, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [1.0] * 8, - "expected_with_subtrees": [1.0] * 5, + "expected_wout_subtrees": [1.0] * 14, + "expected_with_subtrees": [1.0] * 9, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.0] * 3, + "expected_with_subtrees": [1.0] * 5, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1.0] * 3, - "expected_with_subtrees": [1.0] * 3, + "expected_wout_subtrees": [1.0] * 5, + "expected_with_subtrees": [1.0] * 5, } ], "section_radial_distances": [ @@ -802,29 +831,35 @@ def _neurite_features(mode): # the root of the subtree is considered "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [1.0, 2.0, 1.4142135, 1.4142135, 3.1622777, 2.828427, - 3.6055512, 3.6055512, 1.0, 2.0, 1.4142135], + [1. , 2. , 2.236068, 2.236068, 1.414214] + + [1.414214, 3.162278, 3.316625, 3.316625] + + [2.828427, 3.605551, 3.605551, 3.741657, 3.741657] + + [1., 2., 2.236068, 2.236068, 1.414214], "expected_with_subtrees": - [1.0, 2.0, 1.4142135, 1.4142135, 3.1622777, 1.414214, - 2.236068, 2.236068, 1.0, 2.0, 1.4142135], + [1. , 2. , 2.236068, 2.236068, 1.414214] + + [1.414214, 3.162278, 3.316625, 3.316625] + + [1.414214, 2.236068, 2.236068, 2.44949 , 2.44949] + + [1., 2., 2.236068, 2.236068, 1.414214], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": - [1.0, 2.0, 1.4142135, 1.4142135, 3.1622777, 2.828427, - 3.6055512, 3.6055512], + [1. , 2. , 2.236068, 2.236068, 1.414214] + + [1.414214, 3.162278, 3.316625, 3.316625] + + [2.828427, 3.605551, 3.605551, 3.741657, 3.741657], "expected_with_subtrees": - [1.0, 2.0, 1.4142135, 1.4142135, 3.1622777], + [1. , 2. , 2.236068, 2.236068, 1.414214] + + [1.414214, 3.162278, 3.316625, 3.316625], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.414214, 2.236068, 2.236068], + "expected_with_subtrees": [1.414214, 2.236068, 2.236068, 2.44949 , 2.44949], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1., 2., 1.414214], - "expected_with_subtrees": [1., 2., 1.414214], + "expected_wout_subtrees": [1., 2., 2.236068, 2.236068, 1.414214], + "expected_with_subtrees": [1., 2., 2.236068, 2.236068, 1.414214], } ], @@ -832,24 +867,35 @@ def _neurite_features(mode): { "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [2.0, 1.4142135, 3.1622777, 3.6055512, 3.6055512, 2.0, 1.4142135], + [2.236068, 2.236068, 1.414214] + + [3.316625, 3.316625] + + [3.605551, 3.741657, 3.741657] + + [2.236068, 2.236068, 1.414214], "expected_with_subtrees": - [2.0, 1.4142135, 3.1622777, 2.236068, 2.236068, 2.0, 1.4142135], + [2.236068, 2.236068, 1.414214] + + [3.316625, 3.316625] + + [2.236068, 2.44949 , 2.44949] + + [2.236068, 2.236068, 1.414214], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [2.0, 1.4142135, 3.1622777, 3.6055512, 3.6055512], - "expected_with_subtrees": [2.0, 1.4142135, 3.1622777], + "expected_wout_subtrees": + [2.236068, 2.236068, 1.414214] + + [3.316625, 3.316625] + + [3.605551, 3.741657, 3.741657], + "expected_with_subtrees": + [2.236068, 2.236068, 1.414214] + + [3.316625, 3.316625], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [2.236068, 2.236068], + "expected_with_subtrees": [2.236068, 2.44949 , 2.44949], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [2., 1.414214], - "expected_with_subtrees": [2., 1.414214], + "expected_wout_subtrees": [2.236068, 2.236068, 1.414214], + "expected_with_subtrees": [2.236068, 2.236068, 1.414214], } ], @@ -859,331 +905,372 @@ def _neurite_features(mode): # the root of the subtree is considered instead of the tree root # heterogeneous forks are not valid forking points "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [1.0, 1.4142135, 2.828427, 1.0], - "expected_with_subtrees": [1.0, 1.4142135, 1.0], + "expected_wout_subtrees": + [1., 2., 1.414214, 3.162278, 2.828427, 3.605551, 1., 2.], + "expected_with_subtrees": + [1., 2., 3.162278, 1.414214, 2.236068, 1., 2.], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [1.0, 1.4142135, 2.828427], - "expected_with_subtrees": [1.0], + "expected_wout_subtrees": + [1., 2., 1.414214, 3.162278, 2.828427, 3.605551], + "expected_with_subtrees": [1., 2., 3.162278], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.4142135,], + "expected_with_subtrees": [1.414214, 2.236068], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1.], - "expected_with_subtrees": [1.], + "expected_wout_subtrees": [1., 2.], + "expected_with_subtrees": [1., 2.], } ], "section_end_distances": [ { "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + [1.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.] + + [1.] * 5, "expected_with_subtrees": - [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + [1.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.] + + [1.] * 5, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": - [1., 1., 1., 1.414214, 2., 1.414214, 1., 1.], + [1.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.], "expected_with_subtrees": - [1., 1., 1., 1.414214, 2.], + [1.] * 5 + + [1.414214, 2., 1., 1.], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.414214, 1., 1.], + "expected_with_subtrees": [1.414214, 1., 1., 1., 1.], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1., 1., 1.], - "expected_with_subtrees": [1., 1., 1.], + "expected_wout_subtrees": [1.] * 5, + "expected_with_subtrees": [1.] * 5, } ], "section_term_lengths": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [1., 1., 2., 1., 1., 1., 1.], - "expected_with_subtrees": [1., 1., 2., 1., 1., 1., 1.], + "expected_wout_subtrees": [1.] * 11, + "expected_with_subtrees": [1.] * 11, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [1., 1., 2., 1., 1.], - "expected_with_subtrees": [1., 1., 2.], + "expected_wout_subtrees": [1.] * 8, + "expected_with_subtrees": [1.] * 5, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1., 1.], + "expected_with_subtrees": [1.] * 3, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1., 1.], - "expected_with_subtrees": [1., 1.], + "expected_wout_subtrees": [1.] * 3, + "expected_with_subtrees": [1.] * 3, } ], "section_taper_rates": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [0.0] * 11, - "expected_with_subtrees": [0.0] * 11, + "expected_wout_subtrees": [0.0] * 19, + "expected_with_subtrees": [0.0] * 19, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [0.0] * 8, - "expected_with_subtrees": [0.0] * 5, + "expected_wout_subtrees": [0.0] * 14, + "expected_with_subtrees": [0.0] * 9, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.0] * 3, + "expected_with_subtrees": [0.0] * 5, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.0] * 3, - "expected_with_subtrees": [0.0] * 3, + "expected_wout_subtrees": [0.0] * 5, + "expected_with_subtrees": [0.0] * 5, } ], "section_bif_lengths": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [1., 1.414214, 1.414214, 1.], - "expected_with_subtrees": [1., 1.414214, 1.], + "expected_wout_subtrees": + [1., 1., 1.414214, 2., 1.414214, 1., 1., 1.], + "expected_with_subtrees": + [1., 1., 2., 1.414214, 1., 1., 1.], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [1., 1.414214, 1.414214], - "expected_with_subtrees": [1.], + "expected_wout_subtrees": [1., 1., 1.414214, 2., 1.414214, 1.], + "expected_with_subtrees": [1., 1., 2.], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.414214], + "expected_with_subtrees": [1.414214, 1.], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1.], - "expected_with_subtrees": [1.], + "expected_wout_subtrees": [1., 1.], + "expected_with_subtrees": [1., 1.], }, ], "section_branch_orders": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [0, 1, 1, 0, 1, 1, 2, 2, 0, 1, 1], - "expected_with_subtrees": [0, 1, 1, 0, 1, 1, 2, 2, 0, 1, 1], + "expected_wout_subtrees": + [0, 1, 2, 2, 1, 0, 1, 2, 2, 1, 2, 2, 3, 3, 0, 1, 2, 2, 1], + "expected_with_subtrees": + [0, 1, 2, 2, 1, 0, 1, 2, 2, 1, 2, 2, 3, 3, 0, 1, 2, 2, 1], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [0, 1, 1, 0, 1, 1, 2, 2], - "expected_with_subtrees": [0, 1, 1, 0, 1], + "expected_wout_subtrees": [0, 1, 2, 2, 1, 0, 1, 2, 2, 1, 2, 2, 3, 3], + "expected_with_subtrees": [0, 1, 2, 2, 1, 0, 1, 2, 2], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1, 2, 2], + "expected_with_subtrees": [1, 2, 2, 3, 3], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0, 1, 1], - "expected_with_subtrees": [0, 1, 1], + "expected_wout_subtrees": [0, 1, 2, 2, 1], + "expected_with_subtrees": [0, 1, 2, 2, 1], }, ], "section_bif_branch_orders": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [0, 0, 1, 0], - "expected_with_subtrees": [0, 1, 0], + "expected_wout_subtrees": [0, 1, 0, 1, 1, 2, 0, 1], + "expected_with_subtrees": [0, 1, 1, 1, 2, 0, 1], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [0, 0, 1], - "expected_with_subtrees": [0], + "expected_wout_subtrees": [0, 1, 0, 1, 1, 2], + "expected_with_subtrees": [0, 1, 1], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1], + "expected_with_subtrees": [1, 2], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0], - "expected_with_subtrees": [0], + "expected_wout_subtrees": [0, 1], + "expected_with_subtrees": [0, 1], }, ], "section_term_branch_orders": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [1, 1, 1, 2, 2, 1, 1], - "expected_with_subtrees": [1, 1, 1, 2, 2, 1, 1], + "expected_wout_subtrees": [2, 2, 1, 2, 2, 2, 3, 3, 2, 2, 1], + "expected_with_subtrees": [2, 2, 1, 2, 2, 2, 3, 3, 2, 2, 1], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [1, 1, 1, 2, 2], - "expected_with_subtrees": [1, 1, 1], + "expected_wout_subtrees": [2, 2, 1, 2, 2, 2, 3, 3], + "expected_with_subtrees": [2, 2, 1, 2, 2], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [2, 2], + "expected_with_subtrees": [2, 3, 3], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1, 1], - "expected_with_subtrees": [1, 1], + "expected_wout_subtrees": [2, 2, 1], + "expected_with_subtrees": [2, 2, 1], }, ], "section_strahler_orders": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [2, 1, 1, 2, 1, 2, 1, 1, 2, 1, 1], - "expected_with_subtrees": [2, 1, 1, 2, 1, 2, 1, 1, 2, 1, 1], + "expected_wout_subtrees": + [2, 2, 1, 1, 1, 3, 2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1], + "expected_with_subtrees": + [2, 2, 1, 1, 1, 3, 2, 1, 1, 2, 1, 2, 1, 1, 2, 2, 1, 1, 1], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [2, 1, 1, 2, 1, 2, 1, 1], - "expected_with_subtrees": [2, 1, 1, 2, 1], + "expected_wout_subtrees": [2, 2, 1, 1, 1, 3, 2, 1, 1, 2, 1, 2, 1, 1], + "expected_with_subtrees": [2, 2, 1, 1, 1, 3, 2, 1, 1], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [2, 1, 1], + "expected_with_subtrees": [2, 1, 2, 1, 1], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [2, 1, 1], - "expected_with_subtrees": [2, 1, 1], + "expected_wout_subtrees": [2, 2, 1, 1, 1], + "expected_with_subtrees": [2, 2, 1, 1, 1], }, ], "segment_lengths": [ { "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + [1.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.] + + [1.] * 5, "expected_with_subtrees": - [1., 1., 1., 1.414214, 2., 1.414214, 1., 1., 1., 1., 1.], + [1.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.] + + [1.] * 5, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": - [1., 1., 1., 1.414214, 2., 1.414214, 1., 1.], + [1.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.], "expected_with_subtrees": - [1., 1., 1., 1.414214, 2.], + [1.] * 5 + + [1.414214, 2., 1., 1.], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.414214, 1., 1.], + "expected_with_subtrees": [1.414214, 1., 1., 1., 1.], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1., 1., 1.], - "expected_with_subtrees": [1., 1., 1.], + "expected_wout_subtrees": [1.] * 5, + "expected_with_subtrees": [1.] * 5, } ], "segment_areas": [ { "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, - 0.628318, 0.628318, 0.628318, 0.628318, 0.628318], + [0.628319] * 5 + + [0.888577, 1.256637, 0.628319, 0.628319] + + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319] + + [0.628319] * 5, "expected_with_subtrees": - [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, - 0.628318, 0.628318, 0.628318, 0.628318, 0.628318], + [0.628319] * 5 + + [0.888577, 1.256637, 0.628319, 0.628319] + + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319] + + [0.628319] * 5, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": - [0.628318, 0.628319, 0.628319, 0.888577, 1.256637, 0.888576, - 0.628318, 0.628318], + [0.628319] * 5 + + [0.888577, 1.256637, 0.628319, 0.628319] + + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319], "expected_with_subtrees": - [0.628318, 0.628319, 0.628319, 0.888577, 1.256637], + [0.628319] * 5 + + [0.888577, 1.256637, 0.628319, 0.628319], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.888576, 0.628318, 0.628318], + "expected_with_subtrees": + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.628318, 0.628318, 0.628318], - "expected_with_subtrees": [0.628318, 0.628318, 0.628318], + "expected_wout_subtrees": [0.628318] * 5, + "expected_with_subtrees": [0.628318] * 5, } ], "segment_volumes": [ { "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, - 0.031415, 0.031415, 0.031415, 0.031415], + [0.031415] * 5 + + [0.044429, 0.062832, 0.031416, 0.031416] + + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416] + + [0.031416] * 5, "expected_with_subtrees": - [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, - 0.031415, 0.031415, 0.031415, 0.031415], + [0.031415] * 5 + + [0.044429, 0.062832, 0.031416, 0.031416] + + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416] + + [0.031416] * 5, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": - [0.031415, 0.031415, 0.031415, 0.044428, 0.062831, 0.044428, 0.031415, - 0.031415], + [0.031415] * 5 + + [0.044429, 0.062832, 0.031416, 0.031416] + + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416], "expected_with_subtrees": - [0.031415, 0.031415, 0.031415, 0.044428, 0.062831], + [0.031415] * 5 + + [0.044429, 0.062832, 0.031416, 0.031416], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.044428, 0.031415, 0.031415], + "expected_with_subtrees": + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.031415, 0.031415, 0.031415], - "expected_with_subtrees": [0.031415, 0.031415, 0.031415], + "expected_wout_subtrees": [0.031415] * 5, + "expected_with_subtrees": [0.031415] * 5, } ], "segment_radii": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [0.1] * 11, - "expected_with_subtrees": [0.1] * 11, + "expected_wout_subtrees": [0.1] * 19, + "expected_with_subtrees": [0.1] * 19, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [0.1] * 8, - "expected_with_subtrees": [0.1] * 5, + "expected_wout_subtrees": [0.1] * 14, + "expected_with_subtrees": [0.1] * 9, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.1] * 3, + "expected_with_subtrees": [0.1] * 5, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.1] * 3, - "expected_with_subtrees": [0.1] * 3, + "expected_wout_subtrees": [0.1] * 5, + "expected_with_subtrees": [0.1] * 5, } ], "segment_taper_rates": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [0.0] * 11, - "expected_with_subtrees": [0.0] * 11, + "expected_wout_subtrees": [0.0] * 19, + "expected_with_subtrees": [0.0] * 19, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [0.0] * 8, - "expected_with_subtrees": [0.0] * 5, + "expected_wout_subtrees": [0.0] * 14, + "expected_with_subtrees": [0.0] * 9, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.0] * 3, + "expected_with_subtrees": [0.0] * 5, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.0] * 3, - "expected_with_subtrees": [0.0] * 3, + "expected_wout_subtrees": [0.0] * 5, + "expected_with_subtrees": [0.0] * 5, }, ], "segment_radial_distances": [ @@ -1192,60 +1279,80 @@ def _neurite_features(mode): # the root of the subtree is considered "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": - [0.5, 1.5, 1.118034, 0.70710677, 2.236068, 2.1213202, 3.2015622, 3.2015622, - 0.5, 1.5, 1.118034], + [0.5, 1.5, 2.061553, 2.061553, 1.118034] + + [0.707107, 2.236068, 3.201562, 3.201562] + + [2.12132 , 3.201562, 3.201562, 3.640055, 3.640055] + + [0.5, 1.5, 2.061553, 2.061553, 1.118034], "expected_with_subtrees": - [0.5, 1.5, 1.118034, 0.70710677, 2.236068, 0.707107, 1.802776, 1.802776, - 0.5, 1.5, 1.118034], + [0.5, 1.5, 2.061553, 2.061553, 1.118034] + + [0.707107, 2.236068, 3.201562, 3.201562] + + [0.707107, 1.802776, 1.802776, 2.291288, 2.291288] + + [0.5, 1.5, 2.061553, 2.061553, 1.118034], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": - [0.5, 1.5, 1.118034, 0.70710677, 2.236068, 2.1213202, 3.2015622, 3.2015622], + [0.5, 1.5, 2.061553, 2.061553, 1.118034] + + [0.707107, 2.236068, 3.201562, 3.201562] + + [2.12132 , 3.201562, 3.201562, 3.640055, 3.640055], "expected_with_subtrees": - [0.5, 1.5, 1.118034, 0.70710677, 2.236068], + [0.5, 1.5, 2.061553, 2.061553, 1.118034] + + [0.707107, 2.236068, 3.201562, 3.201562], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.707107, 1.802776, 1.802776], + "expected_with_subtrees": [0.707107, 1.802776, 1.802776, 2.291288, 2.291288], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.5, 1.5, 1.118034], - "expected_with_subtrees": [0.5, 1.5, 1.118034], + "expected_wout_subtrees": [0.5, 1.5, 2.061553, 2.061553, 1.118034], + "expected_with_subtrees": [0.5, 1.5, 2.061553, 2.061553, 1.118034], }, ], "segment_midpoints": [ { "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [ - [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-2.0, 0.5, 0.0], [0.5, 1.5, 0. ], - [1.0, 3.0, 0.0], [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0], - [0.0, -1.5, 0.0], [0., -2.5, 0.0], [0.5, -2.0, 0.0]], + [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-3.0, 0.0, 0.5], [-3.0, 0.0, -0.5], + [-2.0, 0.5, 0.0], [0.5, 1.5, 0.0], [1.0, 3.0, 0.0], [1.0, 4.0, 0.5], + [1.0, 4.0, -0.5], [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0], + [3.0, 3.0, 0.5], [3.0, 3.0, -0.5], [0.0, -1.5, 0.0], [0.0, -2.5, 0.0], + [0.0, -3.0, 0.5], [0.0, -3.0, -0.5], [0.5, -2.0, 0.0]], "expected_with_subtrees": [ - [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-2.0, 0.5, 0.0], [0.5, 1.5, 0. ], - [1.0, 3.0, 0.0], [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0], - [0.0, -1.5, 0.0], [0., -2.5, 0.0], [0.5, -2.0, 0.0]], + [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-3.0, 0.0, 0.5], [-3.0, 0.0, -0.5], + [-2.0, 0.5, 0.0], [0.5, 1.5, 0.0], [1.0, 3.0, 0.0], [1.0, 4.0, 0.5], + [1.0, 4.0, -0.5], [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0], + [3.0, 3.0, 0.5], [3.0, 3.0, -0.5], [0.0, -1.5, 0.0], [0.0, -2.5, 0.0], + [0.0, -3.0, 0.5], [0.0, -3.0, -0.5], [0.5, -2.0, 0.0]], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [ - [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-2.0, 0.5, 0.0], [0.5, 1.5, 0. ], - [1.0, 3.0, 0.0], [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0]], + [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-3.0, 0.0, 0.5], [-3.0, 0.0, -0.5], + [-2.0, 0.5, 0.0], [0.5, 1.5, 0.0], [1.0, 3.0, 0.0], [1.0, 4.0, 0.5], + [1.0, 4.0, -0.5], [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0], + [3.0, 3.0, 0.5], [3.0, 3.0, -0.5]], "expected_with_subtrees": [ - [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-2.0, 0.5, 0.0], [0.5, 1.5, 0. ], - [1.0, 3.0, 0.0]], + [-1.5, 0.0, 0.0], [-2.5, 0.0, 0.0], [-3.0, 0.0, 0.5], [-3.0, 0.0, -0.5], + [-2.0, 0.5, 0.0], [0.5, 1.5, 0.0], [1.0, 3.0, 0.0], [1.0, 4.0, 0.5], + [1.0, 4.0, -0.5]], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [[1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0]], + "expected_with_subtrees": [ + [1.5, 2.5, 0.0], [2.0, 3.5, 0.0], [2.5, 3.0, 0.0], + [3.0, 3.0, 0.5], [3.0, 3.0, -0.5]], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [[0.0, -1.5, 0.0], [0., -2.5, 0.0], [0.5, -2.0, 0.0]], - "expected_with_subtrees": [[0.0, -1.5, 0.0], [0., -2.5, 0.0], [0.5, -2.0, 0.0]], + "expected_wout_subtrees": [ + [0.0, -1.5, 0.0], [0.0, -2.5, 0.0], [0.0, -3.0, 0.5], + [0.0, -3.0, -0.5], [0.5, -2.0, 0.0]], + "expected_with_subtrees": [ + [0.0, -1.5, 0.0], [0.0, -2.5, 0.0], [0.0, -3.0, 0.5], + [0.0, -3.0, -0.5], [0.5, -2.0, 0.0]], }, ], "segment_meander_angles": [ @@ -1273,221 +1380,234 @@ def _neurite_features(mode): "number_of_sections": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 11, - "expected_with_subtrees": 11, + "expected_wout_subtrees": 19, + "expected_with_subtrees": 19, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 8, - "expected_with_subtrees": 5, + "expected_wout_subtrees": 14, + "expected_with_subtrees": 9, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0, - "expected_with_subtrees": 3, + "expected_with_subtrees": 5, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": 3, - "expected_with_subtrees": 3, + "expected_wout_subtrees": 5, + "expected_with_subtrees": 5, }, ], "number_of_bifurcations": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 4, - "expected_with_subtrees": 3, + "expected_wout_subtrees": 8, + "expected_with_subtrees": 7, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 3, - "expected_with_subtrees": 1, - }, - { - "kwargs": {"neurite_type": NeuriteType.axon}, - "expected_wout_subtrees": 0, - "expected_with_subtrees": 1, - }, - { - "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": 1, - "expected_with_subtrees": 1, - }, - ], - "number_of_forking_points": [ - { - "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": 4, + "expected_wout_subtrees": 6, "expected_with_subtrees": 3, }, - { - "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": 3, - "expected_with_subtrees": 1, - }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0, - "expected_with_subtrees": 1, + "expected_with_subtrees": 2, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": 1, - "expected_with_subtrees": 1, + "expected_wout_subtrees": 2, + "expected_with_subtrees": 2, }, ], - "volume_density": [ # neurites are flat :( + "number_of_forking_points": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": np.nan, - "expected_with_subtrees": np.nan, + "expected_wout_subtrees": 8, + "expected_with_subtrees": 7, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": np.nan, - "expected_with_subtrees": np.nan, + "expected_wout_subtrees": 6, + "expected_with_subtrees": 3, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": 0, - "expected_with_subtrees": np.nan, + "expected_with_subtrees": 2, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": np.nan, - "expected_with_subtrees": np.nan, + "expected_wout_subtrees": 2, + "expected_with_subtrees": 2, }, ], +# "volume_density": [ +# { +# "kwargs": {"neurite_type": NeuriteType.all}, +# "expected_wout_subtrees": 0.5350236198351997, +# "expected_with_subtrees": 0.5350236198351997, +# }, +# { +# "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, +# "expected_wout_subtrees": np.nan, +# "expected_with_subtrees": np.nan, +# }, +# { +# "kwargs": {"neurite_type": NeuriteType.axon}, +# "expected_wout_subtrees": 0, +# "expected_with_subtrees": np.nan, +# }, +# { +# "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, +# "expected_wout_subtrees": np.nan, +# "expected_with_subtrees": np.nan, +# }, +# ], "local_bifurcation_angles": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [0.5 * np.pi, 0.785398, 0.5 * np.pi, 0.5 * np.pi], - "expected_with_subtrees": [0.5 * np.pi, 0.5 * np.pi, 0.5 * np.pi], + "expected_wout_subtrees": + [1.570796, 3.141593, 0.785398, 3.141593, + 1.570796, 3.141593, 1.570796, 3.141593], + "expected_with_subtrees": + [1.570796, 3.141593, 3.141593, 1.570796, 3.141593, 1.570796, 3.141593], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [0.5 * np.pi, 0.785398, 0.5 * np.pi], - "expected_with_subtrees": [1.570796], + "expected_wout_subtrees": + [1.570796, 3.141593, 0.785398, 3.141593, 1.570796, 3.141593], + "expected_with_subtrees": [1.570796, 3.141593, 3.141593], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.5 * np.pi], + "expected_with_subtrees": [1.570796, 3.141593], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.5 * np.pi], - "expected_with_subtrees": [0.5 * np.pi], + "expected_wout_subtrees": [1.570796, 3.141593], + "expected_with_subtrees": [1.570796, 3.141593], }, ], "remote_bifurcation_angles": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [0.5 * np.pi, 0.785398, 0.5 * np.pi, 0.5 * np.pi], - "expected_with_subtrees": [0.5 * np.pi, 0.5 * np.pi, 0.5 * np.pi], + "expected_wout_subtrees": + [1.570796, 3.141593, 0.785398, 3.141593, + 1.570796, 3.141593, 1.570796, 3.141593], + "expected_with_subtrees": + [1.570796, 3.141593, 3.141593, 1.570796, 3.141593, 1.570796, 3.141593], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [0.5 * np.pi, 0.785398, 0.5 * np.pi], - "expected_with_subtrees": [1.570796], + "expected_wout_subtrees": + [1.570796, 3.141593, 0.785398, 3.141593, 1.570796, 3.141593], + "expected_with_subtrees": [1.570796, 3.141593, 3.141593], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [0.5 * np.pi], + "expected_with_subtrees": [1.570796, 3.141593], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [0.5 * np.pi], - "expected_with_subtrees": [0.5 * np.pi], + "expected_wout_subtrees": [1.570796, 3.141593], + "expected_with_subtrees": [1.570796, 3.141593], }, ], "sibling_ratios": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [1.0] * 4, - "expected_with_subtrees": [1.0] * 3, + "expected_wout_subtrees": [1.0] * 8, + "expected_with_subtrees": [1.0] * 7, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [1.0] * 3, - "expected_with_subtrees": [1.0], + "expected_wout_subtrees": [1.0] * 6, + "expected_with_subtrees": [1.0] * 3, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.0], + "expected_with_subtrees": [1.0] * 2, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1.0], - "expected_with_subtrees": [1.0], + "expected_wout_subtrees": [1.0] * 2, + "expected_with_subtrees": [1.0] * 2, }, ], "partition_pairs": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [(1.0, 1.0), (1.0, 3.0), (1.0, 1.0), (1.0, 1.0)], - "expected_with_subtrees": [(1.0, 1.0), (1.0, 1.0), (1.0, 1.0)], + "expected_wout_subtrees": + [[3.0, 1.0], [1.0, 1.0], [3.0, 5.0], + [1.0, 1.0], [1.0, 3.0], [1.0, 1.0], [3.0, 1.0], [1.0, 1.0]], + "expected_with_subtrees": + [[3.0, 1.0], [1.0, 1.0], [1.0, 1.0], + [1.0, 3.0], [1.0, 1.0], [3.0, 1.0], [1.0, 1.0]], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [(1.0, 1.0), (1.0, 3.0), (1.0, 1.0)], - "expected_with_subtrees": [(1.0, 1.0)], + "expected_wout_subtrees": + [[3.0, 1.0], [1.0, 1.0], [3.0, 5.0], [1.0, 1.0], [1.0, 3.0], [1.0, 1.0]], + "expected_with_subtrees": [[3.0, 1.0], [1.0, 1.0], [1.0, 1.0]], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [(1.0, 1.0)], + "expected_with_subtrees": [[1.0, 3.0], [1.0, 1.0]], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [(1.0, 1.0)], - "expected_with_subtrees": [(1.0, 1.0)], + "expected_wout_subtrees": [[3.0, 1.0], [1.0, 1.0]], + "expected_with_subtrees": [[3.0, 1.0], [1.0, 1.0]], }, ], "diameter_power_relations": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [2.0] * 4, - "expected_with_subtrees": [2.0] * 3, + "expected_wout_subtrees": [2.0] * 8, + "expected_with_subtrees": [2.0] * 7, }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [2.0] * 3, - "expected_with_subtrees": [2.0], + "expected_wout_subtrees": [2.0] * 6, + "expected_with_subtrees": [2.0] * 3, }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [2.0], + "expected_with_subtrees": [2.0] * 2, }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [2.0], - "expected_with_subtrees": [2.0], + "expected_wout_subtrees": [2.0] * 2, + "expected_with_subtrees": [2.0] * 2, }, ], "bifurcation_partitions": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [1.0, 3.0, 1.0, 1.0], - "expected_with_subtrees": [1.0] * 3, + "expected_wout_subtrees": [3., 1., 1.666667, 1., 3., 1., 3., 1.], + "expected_with_subtrees": [3., 1., 1., 3., 1., 3., 1.], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [1.0, 3.0, 1.0], - "expected_with_subtrees": [1.0], + "expected_wout_subtrees": [3., 1., 1.666667, 1., 3., 1. ], + "expected_with_subtrees": [3., 1., 1.], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.0], + "expected_with_subtrees": [3., 1.], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [1.0], - "expected_with_subtrees": [1.0], + "expected_wout_subtrees": [3., 1.], + "expected_with_subtrees": [3., 1.], }, ], "section_path_distances": [ @@ -1495,80 +1615,125 @@ def _neurite_features(mode): # subtree path distances are calculated to the root of the subtree "kwargs": {"neurite_type": NeuriteType.all}, "expected_wout_subtrees": [ - 1.0, 2.0, 2.0, 1.414213, 3.414216, 2.828427, 3.828427, 3.828427, 1.0, 2.0, 2.0 + 1.0, 2.0, 3.0, 3.0, 2.0, 1.414213, 3.414213, 4.414213, + 4.414213, 2.828427, 3.828427, 3.828427, 4.828427, 4.828427, + 1.0, 2.0, 3.0, 3.0, 2.0 ], "expected_with_subtrees": [ - 1.0, 2.0, 2.0, 1.414213, 3.414216, 1.414213, 2.414213, 2.414213, 1.0, 2.0, 2.0 - ] - + 1., 2., 3., 3., 2., 1.414214, 3.414214, 4.414214, 4.414214, 1.414214, + 2.414214, 2.414214, 3.414214, 3.414214, 1., 2., 3., 3., 2. + ], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, "expected_wout_subtrees": [ - 1.0, 2.0, 2.0, 1.414213, 3.414216, 2.828427, 3.828427, 3.828427], - "expected_with_subtrees": [1.0, 2.0, 2.0, 1.414213, 3.414216], + 1., 2., 3., 3., 2., 1.414214, 3.414214, 4.414214, 4.414214, + 2.828427, 3.828427, 3.828427, 4.828427, 4.828427 + ], + "expected_with_subtrees": + [1., 2., 3., 3., 2., 1.414214, 3.414214, 4.414214, 4.414214], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [1.414213, 2.414213, 2.414213], + "expected_with_subtrees": [1.414214, 2.414214, 2.414214, 3.414214, 3.414214], }, ], "terminal_path_lengths": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [2.0, 2.0, 3.414213, 3.828427, 3.828427, 2.0, 2.0], - "expected_with_subtrees": [2.0, 2.0, 3.414213, 2.414213, 2.414213, 2.0, 2.0], + "expected_wout_subtrees": + [3., 3., 2., 4.414214, 4.414214, 3.828427, 4.828427, 4.828427, 3., 3., 2.], + "expected_with_subtrees": + [3., 3., 2., 4.414214, 4.414214, 2.414214, 3.414214, 3.414214, 3., 3., 2.], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [2.0, 2.0, 3.414213, 3.828427, 3.828427], - "expected_with_subtrees": [2.0, 2.0, 3.414213], + "expected_wout_subtrees": + [3., 3., 2., 4.414214, 4.414214, 3.828427, 4.828427, 4.828427], + "expected_with_subtrees": [3., 3., 2., 4.414214, 4.414214], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [2.414213, 2.414213], + "expected_with_subtrees": [2.414214, 3.414214, 3.414214], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [2.0, 2.0], - "expected_with_subtrees": [2.0, 2.0], + "expected_wout_subtrees": [3., 3., 2.], + "expected_with_subtrees": [3., 3., 2.], }, ], "principal_direction_extents": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [2.25, 3.395976, 2.0], - "expected_with_subtrees": [2.25, 2.359056, 2.828427, 2.0], + "expected_wout_subtrees": [3.321543, 5.470702, 3.421831], + "expected_with_subtrees": [3.321543, 2.735383, 3.549779, 3.421831], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [2.25, 3.395976], - "expected_with_subtrees": [2.25, 2.359056], + "expected_wout_subtrees": [3.321543, 5.470702], + "expected_with_subtrees": [3.321543, 2.735383], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [2.828427], + "expected_with_subtrees": [3.549779], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [2.0], - "expected_with_subtrees": [2.0], + "expected_wout_subtrees": [3.421831], + "expected_with_subtrees": [3.421831], }, - ] - + ], + "partition_asymmetry": [ + { + "kwargs": { + "neurite_type": NeuriteType.all, + "variant": "branch-order", + "method": "petilla", + }, + "expected_wout_subtrees": [0.5, 0.0, 0.25, 0.0, 0.5, 0.0, 0.5, 0.0], + "expected_with_subtrees": [0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0], + }, + { + "kwargs": { + "neurite_type": NeuriteType.basal_dendrite, + "variant": "branch-order", + "method": "petilla", + }, + "expected_wout_subtrees": [0.5, 0.0, 0.25, 0.0, 0.5, 0.0], + "expected_with_subtrees": [0.5, 0.0, 0.0], + }, + { + "kwargs": { + "neurite_type": NeuriteType.axon, + "variant": "branch-order", + "method": "petilla", + }, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.5, 0.0], + }, + { + "kwargs": { + "neurite_type": NeuriteType.apical_dendrite, + "variant": "branch-order", + "method": "petilla", + }, + "expected_wout_subtrees": [0.5, 0.0], + "expected_with_subtrees": [0.5, 0.0], + }, + ], } features_not_tested = list( set(_NEURITE_FEATURES) - set(features.keys()) - set(_MORPHOLOGY_FEATURES) ) - assert not features_not_tested, ( - "The following morphology tests need to be included in the tests:\n\n" + - "\n".join(sorted(features_not_tested)) + "\n" - ) + #assert not features_not_tested, ( + # "The following morphology tests need to be included in the tests:\n\n" + + # "\n".join(sorted(features_not_tested)) + "\n" + #) return _dispatch_features(features, mode) @@ -1585,3 +1750,4 @@ def test_morphology__neurite_features_wout_subtrees(feature_name, kwargs, expect ) def test_morphology__neurite_features_with_subtrees(feature_name, kwargs, expected, mixed_morph): _assert_feature_equal(mixed_morph, feature_name, expected, kwargs, use_subtrees=True) + From c3b3ca64236608460a4056fb232c6ffc17325187 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 10 Mar 2022 13:31:01 +0100 Subject: [PATCH 57/87] Add more complex test morph, update features --- neurom/features/morphology.py | 33 +++++++++++++++++++++++--- neurom/features/neurite.py | 11 ++------- tests/test_mixed.py | 44 +++++++++++++++++------------------ 3 files changed, 54 insertions(+), 34 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index a7bdf76b..c3626306 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -685,11 +685,11 @@ def total_height(morph, neurite_type=NeuriteType.all, use_subtrees=False): @feature(shape=()) def total_depth(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Extent of morphology along axis z.""" - return _extent_along_axis(morph, axis=COLS.Z, neurite_type=neurite_type) + return _extent_along_axis(morph, COLS.Z, neurite_type, use_subtrees) @feature(shape=()) -def volume_density(morph, neurite_type=NeuriteType.all): +def volume_density(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Get the volume density. The volume density is defined as the ratio of the neurite volume and @@ -698,11 +698,36 @@ def volume_density(morph, neurite_type=NeuriteType.all): .. note:: Returns `np.nan` if the convex hull computation fails or there are not points available due to neurite type filtering. """ +<<<<<<< HEAD # note: duplicate points are present but do not affect convex hull calculation points = [ point for point_list in iter_neurites(morph, mapfun=sf.section_points, filt=is_type(neurite_type)) for point in point_list +======= + + def get_points(neurite, section_type=NeuriteType.all): + + if section_type == NeuriteType.all: + return neurite.points[:, COLS.XYZ] + + return [ + point + for section in neurite.root_node.ipreorder() if section.type == section_type + for point in section.points[:, COLS.XYZ] + ] + + # note: duplicate points are present but do not affect convex hull calculation + points = [ + point + for list_of_points in iter_neurites( + morph, + mapfun=get_points, + filt=is_type(neurite_type), + use_subtrees=use_subtrees, + ) + for point in list_of_points +>>>>>>> Add more complex test morph, update features ] if not points: @@ -713,7 +738,9 @@ def volume_density(morph, neurite_type=NeuriteType.all): if morph_hull is None: return np.nan - total_volume = sum(iter_neurites(morph, mapfun=nf.total_volume, filt=is_type(neurite_type))) + total_volume = sum(total_volume_per_neurite( + morph, neurite_type=neurite_type, use_subtrees=use_subtrees) + ) return total_volume / morph_hull.volume diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 9155a4b9..a3caaaf8 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -502,16 +502,9 @@ def get_points(section): chain.from_iterable(_map_sections(get_points, neurite, section_type=section_type)) ) - try: + hull = convex_hull(points) - volume = convex_hull(points).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 + return neurite_volume / hull.volume if hull is not None else np.nan @feature(shape=(...,)) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index f362004b..5cd5b4c3 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -578,6 +578,28 @@ def _morphology_features(mode): "expected_with_subtrees": 2.0, }, ], + "volume_density": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 0.01570426, + "expected_with_subtrees": 0.01570426, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 0.02983588, + "expected_with_subtrees": 0.04907583, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": np.nan, + "expected_with_subtrees": 0.17009254, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 0.23561945, + "expected_with_subtrees": 0.23561945, + }, + ], } features_not_tested = set(_MORPHOLOGY_FEATURES) - set(features.keys()) @@ -1443,28 +1465,6 @@ def _neurite_features(mode): "expected_with_subtrees": 2, }, ], -# "volume_density": [ -# { -# "kwargs": {"neurite_type": NeuriteType.all}, -# "expected_wout_subtrees": 0.5350236198351997, -# "expected_with_subtrees": 0.5350236198351997, -# }, -# { -# "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, -# "expected_wout_subtrees": np.nan, -# "expected_with_subtrees": np.nan, -# }, -# { -# "kwargs": {"neurite_type": NeuriteType.axon}, -# "expected_wout_subtrees": 0, -# "expected_with_subtrees": np.nan, -# }, -# { -# "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, -# "expected_wout_subtrees": np.nan, -# "expected_with_subtrees": np.nan, -# }, -# ], "local_bifurcation_angles": [ { "kwargs": {"neurite_type": NeuriteType.all}, From ca905150f511ffbe05dea61a587d87faea553150 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 14 Mar 2022 00:42:18 +0100 Subject: [PATCH 58/87] Convert all variants of partition asymmetry --- neurom/features/neurite.py | 40 ++++++++++++++--------- neurom/features/section.py | 5 +-- tests/test_mixed.py | 66 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 91 insertions(+), 20 deletions(-) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index a3caaaf8..514e35f5 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -69,7 +69,7 @@ def homogeneous_filter(section): return check_type(section) and Section.is_homogeneous_point(section) if ( - iterator_type in (Section.ibifurcation_point, Section.iforking_point) + iterator_type in {Section.ibifurcation_point, Section.iforking_point} and section_type != NeuriteType.all ): filt = homogeneous_filter @@ -351,15 +351,15 @@ def partition_asymmetry( raise ValueError('Please provide a valid method for partition asymmetry,' 'either "petilla" or "uylings"') - if variant == 'branch-order': + def it_type(section): - def it_type(section): + if section_type == NeuriteType.all: + return Section.ipreorder(section) - if section_type == NeuriteType.all: - return Section.ipreorder(section) + check = is_type(section_type) + return (s for s in section.ipreorder() if check(s)) - check = is_type(section_type) - return (s for s in section.ipreorder() if check(s)) + if variant == 'branch-order': function = partial( bf.partition_asymmetry, uylings=method == 'uylings', iterator_type=it_type @@ -372,22 +372,30 @@ def it_type(section): section_type=section_type ) - 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 + neurite_length = total_length(neurite, section_type=section_type) + + def pathlength_asymmetry_ratio(section): + pathlength_diff = abs( + sf.downstream_pathlength(section.children[0], iterator_type=it_type) - + sf.downstream_pathlength(section.children[1], iterator_type=it_type) + ) + return pathlength_diff / neurite_length + + return _map_sections( + pathlength_asymmetry_ratio, + neurite, + iterator_type=Section.ibifurcation_point, + section_type=section_type + ) @feature(shape=(...,)) -def partition_asymmetry_length(neurite, method='petilla'): +def partition_asymmetry_length(neurite, method='petilla', section_type=NeuriteType.all): """'partition_asymmetry' feature with `variant='length'`. Because it is often used, it has a dedicated feature. """ - return partition_asymmetry(neurite, 'length', method) + return partition_asymmetry(neurite, 'length', method, section_type=section_type) @feature(shape=(...,)) diff --git a/neurom/features/section.py b/neurom/features/section.py index 259699f3..5eeb4129 100644 --- a/neurom/features/section.py +++ b/neurom/features/section.py @@ -33,6 +33,7 @@ from neurom import morphmath as mm from neurom.core.dataformat import COLS from neurom.core.morphology import iter_segments +from neurom.core.morphology import Section from neurom.morphmath import interval_lengths @@ -213,6 +214,6 @@ def section_mean_radius(section): return np.sum(mean_radii * lengths) / np.sum(lengths) -def downstream_pathlength(section): +def downstream_pathlength(section, iterator_type=Section.ipreorder): """Compute the total downstream length starting from a section.""" - return sum(sec.length for sec in section.ipreorder()) + return sum(sec.length for sec in iterator_type(section)) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 5cd5b4c3..6ce0bc8e 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1723,7 +1723,69 @@ def _neurite_features(mode): "expected_wout_subtrees": [0.5, 0.0], "expected_with_subtrees": [0.5, 0.0], }, + { + "kwargs": { + "neurite_type": NeuriteType.all, + "variant": "length", + }, + "expected_wout_subtrees": [0.4, 0.0, 0.130601, 0.0, 0.184699, 0.0, 0.4, 0.0], + "expected_with_subtrees": [0.4, 0.0, 0.0, 0.369398, 0.0, 0.4, 0.0], + }, + { + "kwargs": { + "neurite_type": NeuriteType.basal_dendrite, + "variant": "length", + }, + "expected_wout_subtrees": [0.4, 0.0, 0.130601, 0.0, 0.184699, 0.0], + "expected_with_subtrees": [0.4, 0.0, 0.0], + }, + { + "kwargs": { + "neurite_type": NeuriteType.axon, + "variant": "length", + }, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.369398, 0.0], + }, + { + "kwargs": { + "neurite_type": NeuriteType.apical_dendrite, + "variant": "length", + }, + "expected_wout_subtrees": [0.4, 0.0], + "expected_with_subtrees": [0.4, 0.0], + }, ], + "partition_asymmetry_length": [ + { + "kwargs": { + "neurite_type": NeuriteType.all, + }, + "expected_wout_subtrees": [0.4, 0.0, 0.130601, 0.0, 0.184699, 0.0, 0.4, 0.0], + "expected_with_subtrees": [0.4, 0.0, 0.0, 0.369398, 0.0, 0.4, 0.0], + }, + { + "kwargs": { + "neurite_type": NeuriteType.basal_dendrite, + }, + "expected_wout_subtrees": [0.4, 0.0, 0.130601, 0.0, 0.184699, 0.0], + "expected_with_subtrees": [0.4, 0.0, 0.0], + }, + { + "kwargs": { + "neurite_type": NeuriteType.axon, + }, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.369398, 0.0], + }, + { + "kwargs": { + "neurite_type": NeuriteType.apical_dendrite, + }, + "expected_wout_subtrees": [0.4, 0.0], + "expected_with_subtrees": [0.4, 0.0], + }, + ] } features_not_tested = list( @@ -1737,13 +1799,13 @@ def _neurite_features(mode): return _dispatch_features(features, mode) - +''' @pytest.mark.parametrize( "feature_name, kwargs, expected", _neurite_features(mode="wout-subtrees") ) def test_morphology__neurite_features_wout_subtrees(feature_name, kwargs, expected, mixed_morph): _assert_feature_equal(mixed_morph, feature_name, expected, kwargs, use_subtrees=False) - +''' @pytest.mark.parametrize( "feature_name, kwargs, expected", _neurite_features(mode="with-subtrees") From 3171c304f98954ad5c60273770a36a620110b7f9 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 14 Mar 2022 12:57:29 +0100 Subject: [PATCH 59/87] Convert segment_path_lengths --- neurom/core/morphology.py | 2 +- neurom/features/neurite.py | 28 ++++++++++++++-------------- tests/test_mixed.py | 25 ++++++++++++++++++++----- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index cb49551e..a7582029 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -282,7 +282,7 @@ def extract_subneurites(neurite): neurites = sorted(neurites, key=lambda neurite: NRN_ORDER.get(neurite.type, last_position)) if use_subtrees: - neurites = chain.from_iterable(map(extract_subneurites, neurites)) + neurites = flatten(map(extract_subneurites, neurites)) neurite_iter = iter(neurites) if filt is None else filter(filt, neurites) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 514e35f5..9062c082 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -48,6 +48,7 @@ import numpy as np from neurom import morphmath +from neurom.utils import flatten from neurom.core.types import NeuriteType from neurom.core.morphology import Section from neurom.core.dataformat import COLS @@ -220,11 +221,7 @@ def _map_segments(func, neurite, section_type=NeuriteType.all): `func` accepts a section and returns list of values corresponding to each segment. """ - return [ - segment_value - for section in Section.ipreorder(neurite.root_node) - for segment_value in func(section) - ] + return list(flatten(_map_sections(func, neurite, section_type=section_type))) @feature(shape=(...,)) @@ -283,19 +280,22 @@ def segment_midpoints(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) -def segment_path_lengths(neurite): +def segment_path_lengths(neurite, section_type=NeuriteType.all): """Returns pathlengths between all non-root points and their root point.""" pathlength = {} - def segments_pathlength(section): + def segments_path_length(section): if section.id not in pathlength: - if section.parent: - pathlength[section.id] = section.parent.length + pathlength[section.parent.id] - else: - pathlength[section.id] = 0 + + pathlength[section.id] = ( + 0.0 + if section.id == neurite.root_node.id + else section.parent.length + pathlength[section.parent.id] + ) + return pathlength[section.id] + np.cumsum(sf.segment_lengths(section)) - return _map_segments(segments_pathlength, neurite) + return _map_segments(segments_path_length, neurite, section_type=section_type) @feature(shape=(...,)) @@ -507,7 +507,7 @@ def get_points(section): # note: duplicate points included but not affect the convex hull calculation points = list( - chain.from_iterable(_map_sections(get_points, neurite, section_type=section_type)) + flatten(_map_sections(get_points, neurite, section_type=section_type)) ) hull = convex_hull(points) @@ -552,7 +552,7 @@ def get_points(section): # Note: duplicate points are included and need to be removed points = list( - chain.from_iterable(_map_sections(get_points, neurite, section_type=section_type)) + flatten(_map_sections(get_points, neurite, section_type=section_type)) ) return [morphmath.principal_direction_extent(np.unique(points, axis=0))[direction]] diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 6ce0bc8e..3f4a6d94 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1785,17 +1785,32 @@ def _neurite_features(mode): "expected_wout_subtrees": [0.4, 0.0], "expected_with_subtrees": [0.4, 0.0], }, - ] + ], + "segment_path_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [1.0, 2.0, 3.0, 3.0, 2.0] + + [1.414213, 3.414213, 4.414213, 4.414213] + + [2.828427, 3.828427, 3.828427, 4.828427, 4.828427] + + [1.0, 2.0, 3.0, 3.0, 2.0], + "expected_with_subtrees": + [1.0, 2.0, 3.0, 3.0, 2.0] + + [1.414213, 3.414213, 4.414213, 4.414213] + + [1.414214, 2.414214, 2.414214, 3.414214, 3.414214] + + [1.0, 2.0, 3.0, 3.0, 2.0], + }, + ], } features_not_tested = list( set(_NEURITE_FEATURES) - set(features.keys()) - set(_MORPHOLOGY_FEATURES) ) - #assert not features_not_tested, ( - # "The following morphology tests need to be included in the tests:\n\n" + - # "\n".join(sorted(features_not_tested)) + "\n" - #) + assert not features_not_tested, ( + "The following morphology tests need to be included in the tests:\n\n" + + "\n".join(sorted(features_not_tested)) + "\n" + ) return _dispatch_features(features, mode) From f1a067957d11a5a19a4b9328aae32e42c7f8fa67 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 14 Mar 2022 13:23:35 +0100 Subject: [PATCH 60/87] Uncomment test --- tests/test_mixed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 3f4a6d94..8d6dbab2 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1814,13 +1814,13 @@ def _neurite_features(mode): return _dispatch_features(features, mode) -''' + @pytest.mark.parametrize( "feature_name, kwargs, expected", _neurite_features(mode="wout-subtrees") ) def test_morphology__neurite_features_wout_subtrees(feature_name, kwargs, expected, mixed_morph): _assert_feature_equal(mixed_morph, feature_name, expected, kwargs, use_subtrees=False) -''' + @pytest.mark.parametrize( "feature_name, kwargs, expected", _neurite_features(mode="with-subtrees") From ef87613a4a76fb87af827e837c6ef0c2517624f9 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 14 Mar 2022 14:38:56 +0100 Subject: [PATCH 61/87] Convert population sholl_frequency --- neurom/features/__init__.py | 14 +++++++-- neurom/features/population.py | 34 +++++++++++++++----- tests/test_mixed.py | 58 ++++++++++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 11 deletions(-) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 0f8da7b5..beff6bed 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -99,8 +99,10 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) # 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') + raise NeuroMError( + "Only Neurite, Morphology, Population or list, tuple of Neurite, Morphology" + f"can be used for feature calculation. Got: {obj}" + ) neurite_filter = is_type(kwargs.get('neurite_type', NeuriteType.all)) res, feature_ = None, None @@ -142,9 +144,17 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) # input is a morphology population or a list of morphs if feature_name in _POPULATION_FEATURES: feature_ = _POPULATION_FEATURES[feature_name] + + if "use_subtrees" in inspect.signature(feature_).parameters: + kwargs["use_subtrees"] = use_subtrees + res = feature_(obj, **kwargs) elif feature_name in _MORPHOLOGY_FEATURES: feature_ = _MORPHOLOGY_FEATURES[feature_name] + + if "use_subtrees" in inspect.signature(feature_).parameters: + kwargs["use_subtrees"] = use_subtrees + res = _flatten_feature(feature_.shape, [feature_(n, **kwargs) for n in obj]) elif feature_name in _NEURITE_FEATURES: feature_ = _NEURITE_FEATURES[feature_name] diff --git a/neurom/features/population.py b/neurom/features/population.py index 98cbd4de..4720e3eb 100644 --- a/neurom/features/population.py +++ b/neurom/features/population.py @@ -45,15 +45,18 @@ from neurom.core.dataformat import COLS from neurom.core.types import NeuriteType +from neurom.core.morphology import iter_sections from neurom.core.types import tree_type_checker as is_type from neurom.features import feature, NameSpace -from neurom.features.morphology import sholl_crossings +from neurom.features import morphology as mf feature = partial(feature, namespace=NameSpace.POPULATION) @feature(shape=(...,)) -def sholl_frequency(morphs, neurite_type=NeuriteType.all, step_size=10, bins=None): +def sholl_frequency( + morphs, neurite_type=NeuriteType.all, step_size=10, bins=None, use_subtrees=False +): """Perform Sholl frequency calculations on a population of morphs. Args: @@ -62,6 +65,7 @@ def sholl_frequency(morphs, neurite_type=NeuriteType.all, step_size=10, bins=Non 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``. + use_subtrees (bool): Enable mixed subtree processing. Note: Given a population, the concentric circles range from the smallest soma radius to the @@ -72,13 +76,27 @@ def sholl_frequency(morphs, neurite_type=NeuriteType.all, step_size=10, bins=Non neurite_filter = is_type(neurite_type) if bins is None: + + section_iterator = ( + partial(iter_sections, section_filter=neurite_filter) + if use_subtrees + else partial(iter_sections, neurite_filter=neurite_filter) + ) + + max_radius_per_section = [ + np.max(np.linalg.norm(section.points[:, COLS.XYZ] - morph.soma.center, axis=1)) + for morph in morphs + for section in section_iterator(morph) + ] + + if not max_radius_per_section: + return [] + 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) + + bins = np.arange(min_soma_edge, min_soma_edge + max(max_radius_per_section), step_size) return np.array([ - sholl_crossings(m, neurite_type, m.soma.center, bins) + mf.sholl_crossings(m, neurite_type, m.soma.center, bins, use_subtrees=use_subtrees) for m in morphs - ]).sum(axis=0) + ]).sum(axis=0).tolist() diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 8d6dbab2..59641fa9 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -6,7 +6,8 @@ import numpy.testing as npt from neurom import NeuriteType from neurom.features import get -from neurom.features import _MORPHOLOGY_FEATURES, _NEURITE_FEATURES +from neurom.core import Population +from neurom.features import _POPULATION_FEATURES, _MORPHOLOGY_FEATURES, _NEURITE_FEATURES import collections.abc @@ -46,6 +47,11 @@ def mixed_morph(): reader="swc") +@pytest.fixture +def population(mixed_morph): + return Population([mixed_morph, mixed_morph]) + + def _assert_feature_equal(obj, feature_name, expected_values, kwargs, use_subtrees): def innermost_value(iterable): @@ -102,6 +108,56 @@ def _dispatch_features(features, mode): yield feature_name, kwargs, expected +def _population_features(mode): + + features = { + "sholl_frequency": [ + { + "kwargs": {"neurite_type": NeuriteType.all, "step_size": 3}, + "expected_wout_subtrees": [0, 4], + "expected_with_subtrees": [0, 4], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite, "step_size": 3}, + "expected_wout_subtrees": [0, 4], + "expected_with_subtrees": [0, 2], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon, "step_size": 3}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0, 2], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite, "step_size": 2}, + "expected_wout_subtrees": [0, 2], + "expected_with_subtrees": [0, 2], + }, + + ], + } + + features_not_tested = list( + set(_POPULATION_FEATURES) - set(features.keys()) + ) + + assert not features_not_tested, ( + "The following morphology tests need to be included in the tests:\n\n" + + "\n".join(sorted(features_not_tested)) + "\n" + ) + + return _dispatch_features(features, mode) + + +@pytest.mark.parametrize("feature_name, kwargs, expected", _population_features(mode="wout-subtrees")) +def test_population__population_features_wout_subtrees(feature_name, kwargs, expected, population): + _assert_feature_equal(population, feature_name, expected, kwargs, use_subtrees=False) + + +@pytest.mark.parametrize("feature_name, kwargs, expected", _population_features(mode="with-subtrees")) +def test_population__population_features_with_subtrees(feature_name, kwargs, expected, population): + _assert_feature_equal(population, feature_name, expected, kwargs, use_subtrees=True) + + def _morphology_features(mode): features = { From 7a53411d9461b812855002d9c7ccc0c26d6576f7 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 14 Mar 2022 15:24:11 +0100 Subject: [PATCH 62/87] Increase coverage --- tests/core/test_section.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/core/test_section.py b/tests/core/test_section.py index 25fc4817..93708504 100644 --- a/tests/core/test_section.py +++ b/tests/core/test_section.py @@ -45,6 +45,8 @@ def test_section_base_func(): assert_almost_equal(section.area, 31.41592653589793) assert_almost_equal(section.volume, 15.707963267948964) + # __nonzero__ + assert section def test_section_tree(): m = nm.load_morphology(str(SWC_PATH / 'simple.swc')) From dc41ad9636c28b18d49ad0e3f4b831291fc7d84d Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 15 Mar 2022 23:40:45 +0100 Subject: [PATCH 63/87] Use iter_segments in segment features --- neurom/features/neurite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 9062c082..6d496a9c 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -50,7 +50,7 @@ from neurom import morphmath from neurom.utils import flatten from neurom.core.types import NeuriteType -from neurom.core.morphology import Section +from neurom.core.morphology import Section, iter_segments from neurom.core.dataformat import COLS from neurom.features import NameSpace, feature, bifurcation as bf, section as sf from neurom.morphmath import convex_hull From 916bf0e55200a954fa6bce7a85350cbfb1f2bc63 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 15 Mar 2022 23:46:08 +0100 Subject: [PATCH 64/87] Use section_length from section.py --- neurom/features/neurite.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 6d496a9c..eb83a36b 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -83,6 +83,7 @@ def homogeneous_filter(section): @feature(shape=()) def max_radial_distance(neurite, origin=None, section_type=NeuriteType.all): """Get the maximum radial distances of the termination sections.""" + term_radial_distances = section_term_radial_distances( neurite, origin=origin, section_type=section_type ) @@ -148,22 +149,16 @@ def total_volume(neurite, section_type=NeuriteType.all): return sum(_map_sections(sf.section_volume, neurite, section_type=section_type)) -def _section_length(section): - """Get section length of `section`.""" - return morphmath.section_length(section.points) ->>>>>>> Convert total_volume_per_neurite feature - - @feature(shape=(...,)) def section_lengths(neurite, section_type=NeuriteType.all): """Section lengths.""" - return _map_sections(_section_length, neurite, section_type=section_type) + return _map_sections(sf.section_length, neurite, section_type=section_type) @feature(shape=(...,)) def section_term_lengths(neurite, section_type=NeuriteType.all): """Termination section lengths.""" - return _map_sections(_section_length, neurite, Section.ileaf, section_type) + return _map_sections(sf.section_length, neurite, Section.ileaf, section_type) @feature(shape=(...,)) From 067152e699f9d2ffb23213d3fdbd9efed6426ffc Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 16 Mar 2022 12:09:22 +0100 Subject: [PATCH 65/87] Fix lint --- neurom/features/neurite.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index eb83a36b..4c8bfafa 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -83,7 +83,6 @@ def homogeneous_filter(section): @feature(shape=()) def max_radial_distance(neurite, origin=None, section_type=NeuriteType.all): """Get the maximum radial distances of the termination sections.""" - term_radial_distances = section_term_radial_distances( neurite, origin=origin, section_type=section_type ) From 26eccb7e94e40b6ff976ef753a0297fbbe51144c Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 18 Mar 2022 14:38:01 +0100 Subject: [PATCH 66/87] Fixes from rebase --- neurom/features/morphology.py | 117 +++++++++++----------------------- neurom/features/neurite.py | 14 ++-- 2 files changed, 43 insertions(+), 88 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index c3626306..4877bf88 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# Copyright (c) 2019, Ecole Polytechnique Federale de Lausanne, Blue Brain Project # All rights reserved. # # This file is part of NeuroM @@ -63,9 +63,21 @@ from neurom.core.morphology import Neurite + feature = partial(feature, namespace=NameSpace.NEURON) +def _map_neurites(function, morph, neurite_type, use_subtrees=False): + return list( + iter_neurites( + obj=morph, + mapfun=function, + filt=is_type(neurite_type), + use_subtrees=use_subtrees, + ) + ) + + @feature(shape=()) def soma_volume(morph): """Get the volume of a morphology's soma.""" @@ -91,13 +103,11 @@ def soma_radius(morph): @feature(shape=()) def max_radial_distance(morph, origin=None, neurite_type=NeuriteType.all, use_subtrees=False): """Get the maximum radial distances of the termination sections.""" - term_radial_distances = list( - iter_neurites( - morph, - mapfun=partial(nf.max_radial_distance, origin=origin), - filt=is_type(neurite_type), - use_subtrees=use_subtrees - ) + term_radial_distances = _map_neurites( + partial(nf.max_radial_distance, origin=origin), + morph, + neurite_type=neurite_type, + use_subtrees=use_subtrees, ) return max(term_radial_distances) if term_radial_distances else 0. @@ -105,53 +115,25 @@ def max_radial_distance(morph, origin=None, neurite_type=NeuriteType.all, use_su @feature(shape=(...,)) def number_of_sections_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """List of numbers of sections per neurite.""" - return list( - iter_neurites( - morph, - mapfun=nf.number_of_sections, - filt=is_type(neurite_type), - use_subtrees=use_subtrees - ) - ) + return _map_neurites(nf.number_of_sections, morph, neurite_type, use_subtrees) @feature(shape=(...,)) def total_length_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Neurite lengths.""" - return list( - iter_neurites( - morph, - mapfun=nf.total_length, - filt=is_type(neurite_type), - use_subtrees=use_subtrees - ) - ) + return _map_neurites(nf.total_length, morph, neurite_type, use_subtrees) @feature(shape=(...,)) def total_area_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Neurite areas.""" - return list( - iter_neurites( - morph, - mapfun=nf.total_area, - filt=is_type(neurite_type), - use_subtrees=use_subtrees - ) - ) + return _map_neurites(nf.total_area, morph, neurite_type, use_subtrees) @feature(shape=(...,)) def total_volume_per_neurite(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Neurite volumes.""" - return list( - iter_neurites( - morph, - mapfun=nf.total_volume, - filt=is_type(neurite_type), - use_subtrees=use_subtrees - ) - ) + return _map_neurites(nf.total_volume, morph, neurite_type, use_subtrees) @feature(shape=(...,)) @@ -169,13 +151,7 @@ def azimuth(neurite): morphmath.vector(neurite.root_node.points[0], morph.soma.center) ) - return list( - iter_neurites( - morph, - mapfun=azimuth, - filt=is_type(neurite_type), - ) - ) + return _map_neurites(azimuth, morph, neurite_type, use_subtrees=False) @feature(shape=(...,)) @@ -194,25 +170,16 @@ def elevation(neurite): morphmath.vector(neurite.root_node.points[0], morph.soma.center) ) - return list( - iter_neurites( - morph, - mapfun=elevation, - filt=is_type(neurite_type), - ) - ) + return _map_neurites(elevation, morph, neurite_type, use_subtrees=False) @feature(shape=(...,)) def trunk_vectors(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Calculate the vectors between all the trunks of the morphology and the soma center.""" - def vector_from_soma_to_root(neurite): + def vector_from_soma_to_root(neurite, *_, **__): return morphmath.vector(neurite.root_node.points[0], morph.soma.center) - return [ - vector_from_soma_to_root(n) - for n in iter_neurites(morph, filt=is_type(neurite_type), use_subtrees=use_subtrees) - ] + return _map_neurites(vector_from_soma_to_root, morph, neurite_type, use_subtrees=use_subtrees) @feature(shape=(...,)) @@ -432,7 +399,7 @@ def trunk_origin_radii( * else the mean radius of the points between the given ``min_length_filter`` and ``max_length_filter`` are returned. """ - def trunk_first_radius(neurite): + def trunk_first_radius(neurite, *_, **__): return neurite.root_node.points[0][COLS.R] if min_length_filter is not None and min_length_filter <= 0: @@ -457,7 +424,7 @@ def trunk_first_radius(neurite): "'max_length_filter' value." ) - def trunk_mean_radius(neurite): + def trunk_mean_radius(neurite, *_, **__): points = neurite.root_node.points @@ -495,40 +462,30 @@ def trunk_mean_radius(neurite): else trunk_mean_radius ) - return [ - function(neu) - for neu in iter_neurites(morph, filt=is_type(neurite_type), use_subtrees=use_subtrees) - ] + return _map_neurites(function, morph, neurite_type, use_subtrees) @feature(shape=(...,)) def trunk_section_lengths(morph, neurite_type=NeuriteType.all, use_subtrees=False): """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), use_subtrees=use_subtrees) - ] + def trunk_section_length(neurite, *_, **__): + return morphmath.section_length(neurite.root_node.points) + + return _map_neurites(trunk_section_length, morph, neurite_type, use_subtrees) @feature(shape=()) def number_of_neurites(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Number of neurites in a morph.""" - return sum( - 1 for _ in iter_neurites(morph, filt=is_type(neurite_type), use_subtrees=use_subtrees) - ) + def identity(n, *_, **__): + return n + return len(_map_neurites(identity, morph, neurite_type, use_subtrees)) @feature(shape=(...,)) def neurite_volume_density(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Get volume density per neurite.""" - return list( - iter_neurites( - morph, - mapfun=nf.volume_density, - filt=is_type(neurite_type), - use_subtrees=use_subtrees - ) - ) + return _map_neurites(nf.volume_density, morph, neurite_type, use_subtrees) @feature(shape=(...,)) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 4c8bfafa..dd29d39d 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -1,4 +1,4 @@ - # Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project # All rights reserved. # # This file is part of NeuroM @@ -50,7 +50,7 @@ from neurom import morphmath from neurom.utils import flatten from neurom.core.types import NeuriteType -from neurom.core.morphology import Section, iter_segments +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.morphmath import convex_hull @@ -92,9 +92,7 @@ def max_radial_distance(neurite, origin=None, section_type=NeuriteType.all): @feature(shape=()) def number_of_segments(neurite, section_type=NeuriteType.all): """Number of segments.""" - def count_segments(section): - return len(section.points) - 1 - return sum(_map_sections(count_segments, neurite, section_type=section_type)) + return sum(_map_sections(sf.number_of_segments, neurite, section_type=section_type)) @feature(shape=()) @@ -239,7 +237,7 @@ def segment_volumes(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) def segment_radii(neurite, section_type=NeuriteType.all): """Arithmetic mean of the radii of the points in segments.""" - return _map_segments(sf.segment_radii, neurite, section_type=section_type) + return _map_segments(sf.segment_mean_radii, neurite, section_type=section_type) @feature(shape=(...,)) @@ -270,7 +268,7 @@ def segment_meander_angles(neurite, section_type=NeuriteType.all): @feature(shape=(..., 3)) def segment_midpoints(neurite, section_type=NeuriteType.all): """Return a list of segment mid-points.""" - return _map_segments(sf.segment_midpoints neurite, section_type=section_type) + return _map_segments(sf.segment_midpoints, neurite, section_type=section_type) @feature(shape=(...,)) @@ -302,7 +300,7 @@ def radial_distances(section): mid_pts = 0.5 * (section.points[:-1, COLS.XYZ] + section.points[1:, COLS.XYZ]) return np.linalg.norm(mid_pts - pos[COLS.XYZ], axis=1) - return _map_segments(_radial_distances, neurite, section_type=section_type) + return _map_segments(radial_distances, neurite, section_type=section_type) @feature(shape=(...,)) From 81fb6471917789a12d87374ebfa7446e5c6ae222 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 3 Apr 2022 19:09:10 +0200 Subject: [PATCH 67/87] Convert shape related features --- neurom/core/morphology.py | 29 +++++++++++++ neurom/features/morphology.py | 69 +++++++++++------------------- neurom/features/neurite.py | 10 +---- tests/test_mixed.py | 80 ++++++++++++++++++++++++++++++++--- 4 files changed, 128 insertions(+), 60 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index a7582029..dfa75203 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -360,6 +360,35 @@ def iter_segments( ) +def iter_points( + obj, + neurite_filter=None, + neurite_order=NeuriteIter.FileOrder, + section_filter=None +): + """Return an iterator to the points in a population, morphology, neurites, or section. + + Args: + obj: population, morphology, neurite, section or iterable containing + 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 + section_filter: optional section level filter + """ + sections = ( + iter((obj,)) if isinstance(obj, Section) + else iter_sections( + obj, + neurite_filter=neurite_filter, + neurite_order=neurite_order, + section_filter=section_filter + ) + ) + + return flatten(s.points[:, COLS.XYZ] for s in sections) + + def graft_morphology(section): """Returns a morphology starting at section.""" assert isinstance(section, Section) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 4877bf88..2b2901ab 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -52,17 +52,17 @@ import numpy as np from neurom import morphmath -from neurom.core.morphology import iter_neurites, iter_sections, iter_segments, Morphology +from neurom.core.morphology import ( + iter_neurites, iter_sections, iter_segments, iter_points, 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.exceptions import NeuroMError -from neurom.features import feature, NameSpace, neurite as nf, section as sf +from neurom.features import feature, NameSpace, neurite as nf from neurom.utils import str_to_plane from neurom.morphmath import convex_hull -from neurom.core.morphology import Neurite - feature = partial(feature, namespace=NameSpace.NEURON) @@ -78,6 +78,14 @@ def _map_neurites(function, morph, neurite_type, use_subtrees=False): ) +def _get_points(morph, neurite_type, use_subtrees=False): + return list( + iter_points(morph, section_filter=is_type(neurite_type)) + if use_subtrees + else iter_points(morph, neurite_filter=is_type(neurite_type)) + ) + + @feature(shape=()) def soma_volume(morph): """Get the volume of a morphology's soma.""" @@ -655,37 +663,7 @@ def volume_density(morph, neurite_type=NeuriteType.all, use_subtrees=False): .. note:: Returns `np.nan` if the convex hull computation fails or there are not points available due to neurite type filtering. """ -<<<<<<< HEAD - # note: duplicate points are present but do not affect convex hull calculation - points = [ - point - for point_list in iter_neurites(morph, mapfun=sf.section_points, filt=is_type(neurite_type)) - for point in point_list -======= - - def get_points(neurite, section_type=NeuriteType.all): - - if section_type == NeuriteType.all: - return neurite.points[:, COLS.XYZ] - - return [ - point - for section in neurite.root_node.ipreorder() if section.type == section_type - for point in section.points[:, COLS.XYZ] - ] - - # note: duplicate points are present but do not affect convex hull calculation - points = [ - point - for list_of_points in iter_neurites( - morph, - mapfun=get_points, - filt=is_type(neurite_type), - use_subtrees=use_subtrees, - ) - for point in list_of_points ->>>>>>> Add more complex test morph, update features - ] + points = _get_points(morph, neurite_type, use_subtrees) if not points: return np.nan @@ -702,7 +680,7 @@ def get_points(neurite, section_type=NeuriteType.all): return total_volume / morph_hull.volume -def _unique_projected_points(morph, projection_plane, neurite_type): +def _unique_projected_points(morph, projection_plane, neurite_type, use_subtrees=False): key = "".join(sorted(projection_plane.lower())) @@ -716,9 +694,7 @@ def _unique_projected_points(morph, projection_plane, neurite_type): f"Please select 'xy', 'xz', or 'yz'." ) from e - points = list( - iter_neurites(morph, mapfun=sf.section_points, filt=is_type(neurite_type)) - ) + points = _get_points(morph, neurite_type, use_subtrees) if len(points) == 0: return np.empty(shape=(0, 3), dtype=np.float32) @@ -727,23 +703,24 @@ def _unique_projected_points(morph, projection_plane, neurite_type): @feature(shape=()) -def aspect_ratio(morph, neurite_type=NeuriteType.all, projection_plane="xy"): +def aspect_ratio(morph, neurite_type=NeuriteType.all, projection_plane="xy", use_subtrees=False): """Calculates the min/max ratio of the principal direction extents along the plane. Args: morph: Morphology object. neurite_type: The neurite type to use. By default all neurite types are used. projection_plane: Projection plane to use for the calculation. One of ('xy', 'xz', 'yz'). + use_subtrees: Enable mixed subtree processing Returns: The aspect ratio feature of the morphology points. """ - projected_points = _unique_projected_points(morph, projection_plane, neurite_type) + projected_points = _unique_projected_points(morph, projection_plane, neurite_type, use_subtrees) return np.nan if len(projected_points) == 0 else morphmath.aspect_ratio(projected_points) @feature(shape=()) -def circularity(morph, neurite_type=NeuriteType.all, projection_plane="xy"): +def circularity(morph, neurite_type=NeuriteType.all, projection_plane="xy", use_subtrees=False): """Calculates the circularity of the morphology points along the plane. The circularity is defined as the 4 * pi * area of the convex hull over its @@ -754,16 +731,17 @@ def circularity(morph, neurite_type=NeuriteType.all, projection_plane="xy"): neurite_type: The neurite type to use. By default all neurite types are used. projection_plane: Projection plane to use for the calculation. One of ('xy', 'xz', 'yz'). + use_subtrees: Enable mixed subtree processing Returns: The circularity of the morphology points. """ - projected_points = _unique_projected_points(morph, projection_plane, neurite_type) + projected_points = _unique_projected_points(morph, projection_plane, neurite_type, use_subtrees) return np.nan if len(projected_points) == 0 else morphmath.circularity(projected_points) @feature(shape=()) -def shape_factor(morph, neurite_type=NeuriteType.all, projection_plane="xy"): +def shape_factor(morph, neurite_type=NeuriteType.all, projection_plane="xy", use_subtrees=False): """Calculates the shape factor of the morphology points along the plane. The shape factor is defined as the ratio of the convex hull area over max squared @@ -774,11 +752,12 @@ def shape_factor(morph, neurite_type=NeuriteType.all, projection_plane="xy"): neurite_type: The neurite type to use. By default all neurite types are used. projection_plane: Projection plane to use for the calculation. One of ('xy', 'xz', 'yz'). + use_subtrees: Enable mixed subtree processing Returns: The shape factor of the morphology points. """ - projected_points = _unique_projected_points(morph, projection_plane, neurite_type) + projected_points = _unique_projected_points(morph, projection_plane, neurite_type, use_subtrees) return np.nan if len(projected_points) == 0 else morphmath.shape_factor(projected_points) diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index dd29d39d..1652cbfd 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -50,7 +50,7 @@ from neurom import morphmath from neurom.utils import flatten from neurom.core.types import NeuriteType -from neurom.core.morphology import Section +from neurom.core.morphology import Section, iter_points from neurom.core.dataformat import COLS from neurom.features import NameSpace, feature, bifurcation as bf, section as sf from neurom.morphmath import convex_hull @@ -539,13 +539,7 @@ def principal_direction_extents(neurite, direction=0, section_type=NeuriteType.a Principal direction extents are always sorted in descending order. Therefore, by default the maximal principal direction extent is returned. """ - def get_points(section): - return section.points[:, COLS.XYZ].tolist() - - # Note: duplicate points are included and need to be removed - points = list( - flatten(_map_sections(get_points, neurite, section_type=section_type)) - ) + points = list(iter_points(neurite, section_filter=is_type(section_type))) return [morphmath.principal_direction_extent(np.unique(points, axis=0))[direction]] diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 59641fa9..7c906a2c 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -656,6 +656,72 @@ def _morphology_features(mode): "expected_with_subtrees": 0.23561945, }, ], + "aspect_ratio":[ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 0.630311, + "expected_with_subtrees": 0.630311, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 0.305701, + "expected_with_subtrees": 0.284467, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": np.nan, + "expected_with_subtrees": 0.666667, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 0.5, + "expected_with_subtrees": 0.5, + }, + ], + "circularity": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 0.739583, + "expected_with_subtrees": 0.739583, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 0.525588, + "expected_with_subtrees": 0.483687, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": np.nan, + "expected_with_subtrees": 0.544013, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 0.539012, + "expected_with_subtrees": 0.539012, + }, + ], + "shape_factor": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 0.40566, + "expected_with_subtrees": 0.40566, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 0.21111, + "expected_with_subtrees": 0.18750, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": np.nan, + "expected_with_subtrees": 0.3, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 0.25, + "expected_with_subtrees": 0.25, + }, + ] } features_not_tested = set(_MORPHOLOGY_FEATURES) - set(features.keys()) @@ -1723,23 +1789,23 @@ def _neurite_features(mode): "principal_direction_extents": [ { "kwargs": {"neurite_type": NeuriteType.all}, - "expected_wout_subtrees": [3.321543, 5.470702, 3.421831], - "expected_with_subtrees": [3.321543, 2.735383, 3.549779, 3.421831], + "expected_wout_subtrees": [2., 3.596771, 2.], + "expected_with_subtrees": [2., 3.154926, 2.235207, 2.], }, { "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, - "expected_wout_subtrees": [3.321543, 5.470702], - "expected_with_subtrees": [3.321543, 2.735383], + "expected_wout_subtrees": [2., 3.596771], + "expected_with_subtrees": [2., 3.154926], }, { "kwargs": {"neurite_type": NeuriteType.axon}, "expected_wout_subtrees": [], - "expected_with_subtrees": [3.549779], + "expected_with_subtrees": [2.235207], }, { "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, - "expected_wout_subtrees": [3.421831], - "expected_with_subtrees": [3.421831], + "expected_wout_subtrees": [2.], + "expected_with_subtrees": [2.], }, ], "partition_asymmetry": [ From 68328f5a69d635b6e319c1ef8930baf32cdd9c82 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 5 Apr 2022 16:10:25 +0200 Subject: [PATCH 68/87] Fix _homogeneous_subtrees docstring --- neurom/core/morphology.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index dfa75203..a22cfc72 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -216,12 +216,10 @@ def __repr__(self): def _homogeneous_subtrees(neurite): - """Returns a dictionary the keys of which are section types and the values are the sub-neurites. + """Returns a list of the root nodes of the sub-neurites. A sub-neurite can be either the entire tree or a homogeneous downstream sub-tree. - - Note: Only two different mixed types are allowed """ homogeneous_neurites = {neurite.root_node.type: neurite} for section in neurite.root_node.ipreorder(): From 3dbc9aabf1c9a02226bb082e7ab6fdc8f22ca9fc Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 5 Apr 2022 16:24:06 +0200 Subject: [PATCH 69/87] Add section_filter to docstring --- neurom/core/morphology.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index a22cfc72..f0570f00 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -313,6 +313,8 @@ def iter_sections(neurites, 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 + section_filter: optional section level filter. Please note that neurite_filter takes + precedence over the section_filter. Examples: From 607d5723e8aba792212fd7672aaaf9ba988a0b69 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 5 Apr 2022 16:27:29 +0200 Subject: [PATCH 70/87] Fix year and pylint spacing --- neurom/features/morphology.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index 2b2901ab..f22d2f67 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +# Copyright (c) 2020, Ecole Polytechnique Federale de Lausanne, Blue Brain Project # All rights reserved. # # This file is part of NeuroM @@ -459,7 +459,7 @@ def trunk_mean_radius(neurite, *_, **__): "values excluded all the points of the section so the radius of the first " "point after the 'min_length_filter' path distance is returned." ) - # pylint: disable = invalid-unary-operand-type + # pylint: disable=invalid-unary-operand-type return points[~valid_max, COLS.R][0] return points[valid_pts, COLS.R].mean() From dff754090bc1a5d61701b04ad91747cfb3781128 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 6 Apr 2022 14:45:18 +0200 Subject: [PATCH 71/87] Move extract_subneurites where it is used --- neurom/core/morphology.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index f0570f00..f2438684 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -259,11 +259,6 @@ def iter_neurites( >>> mapping = lambda n : len(n.points) >>> n_points = [n for n in iter_neurites(pop, mapping, filter)] """ - def extract_subneurites(neurite): - if neurite.is_heterogeneous(): - return _homogeneous_subtrees(neurite) - return [neurite] - neurites = ( (obj,) if isinstance(obj, Neurite) @@ -280,7 +275,11 @@ def extract_subneurites(neurite): neurites = sorted(neurites, key=lambda neurite: NRN_ORDER.get(neurite.type, last_position)) if use_subtrees: - neurites = flatten(map(extract_subneurites, neurites)) + + neurites = flatten( + _homogeneous_subtrees(neurite) if neurite.is_heterogeneous() else [neurite] + for neurite in neurites + ) neurite_iter = iter(neurites) if filt is None else filter(filt, neurites) From f3df2c3e3bf9ae314101c08fa159618d5c6d2b59 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 6 Apr 2022 22:00:10 +0200 Subject: [PATCH 72/87] Factor out signature checking using inspect --- neurom/features/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index beff6bed..0d81cb18 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -84,6 +84,11 @@ def _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs, use_subtr ) +def _is_subtree_processing_applicable(feature_function): + """Returns true if feature's signature supports the use_subtrees kwarg.""" + return "use_subtrees" in inspect.signature(feature_function).parameters + + def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs): """Obtain a feature from a set of morphology objects. @@ -130,7 +135,7 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) feature_ = _MORPHOLOGY_FEATURES[feature_name] - if "use_subtrees" in inspect.signature(feature_).parameters: + if _is_subtree_processing_applicable(feature_): kwargs["use_subtrees"] = use_subtrees res = feature_(obj, **kwargs) @@ -145,14 +150,14 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) if feature_name in _POPULATION_FEATURES: feature_ = _POPULATION_FEATURES[feature_name] - if "use_subtrees" in inspect.signature(feature_).parameters: + if _is_subtree_processing_applicable(feature_): kwargs["use_subtrees"] = use_subtrees res = feature_(obj, **kwargs) elif feature_name in _MORPHOLOGY_FEATURES: feature_ = _MORPHOLOGY_FEATURES[feature_name] - if "use_subtrees" in inspect.signature(feature_).parameters: + if _is_subtree_processing_applicable(feature_): kwargs["use_subtrees"] = use_subtrees res = _flatten_feature(feature_.shape, [feature_(n, **kwargs) for n in obj]) From 5bf807628ab3e4ffe92f35a43b77c4a800969bc8 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Wed, 6 Apr 2022 23:22:04 +0200 Subject: [PATCH 73/87] Use **kwargs in _get_neurite_feature_value --- neurom/features/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 0d81cb18..65356970 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -66,7 +66,7 @@ def _flatten_feature(feature_shape, feature_value): return reduce(operator.concat, feature_value, []) -def _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs, use_subtrees): +def _get_neurites_feature_value(feature_, obj, neurite_filter, use_subtrees, **kwargs): """Collects neurite feature values appropriately to feature's shape.""" kwargs.pop('neurite_type', None) # there is no 'neurite_type' arg in _NEURITE_FEATURES @@ -143,7 +143,7 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) elif feature_name in _NEURITE_FEATURES: feature_ = _NEURITE_FEATURES[feature_name] - res = _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs, use_subtrees) + res = _get_neurites_feature_value(feature_, obj, neurite_filter, use_subtrees, **kwargs) elif isinstance(obj, Population) or (is_obj_list and isinstance(obj[0], Morphology)): # input is a morphology population or a list of morphs @@ -166,7 +166,7 @@ def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs) res = _flatten_feature( feature_.shape, [ - _get_neurites_feature_value(feature_, n, neurite_filter, kwargs, use_subtrees) + _get_neurites_feature_value(feature_, n, neurite_filter, use_subtrees, **kwargs) for n in obj ] ) From 873286873afd46c42d3c3be3a9a7b159de8ee28e Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 14 Apr 2022 00:26:56 +0200 Subject: [PATCH 74/87] Add morphology heterogeneous tests --- tests/test_mixed.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 7c906a2c..8522fab9 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -10,6 +10,8 @@ from neurom.features import _POPULATION_FEATURES, _MORPHOLOGY_FEATURES, _NEURITE_FEATURES import collections.abc +from neurom.core import morphology as tested + @pytest.fixture def mixed_morph(): @@ -47,6 +49,61 @@ def mixed_morph(): reader="swc") +def test_heterogeneous_neurite(mixed_morph): + + assert not mixed_morph.neurites[0].is_heterogeneous() + assert mixed_morph.neurites[1].is_heterogeneous() + assert not mixed_morph.neurites[2].is_heterogeneous() + + +def test_is_homogeneous_point(mixed_morph): + + heterogeneous_neurite = mixed_morph.neurites[1] + + sections = list(heterogeneous_neurite.iter_sections()) + + # first section has one axon and one basal children + assert not sections[0].is_homogeneous_point() + + # second section is pure basal + assert sections[1].is_homogeneous_point() + + +def test_homogeneous_subtrees(mixed_morph): + + basal, axon_on_basal, apical = mixed_morph.neurites + + assert tested._homogeneous_subtrees(basal) == [basal] + + sections = list(axon_on_basal.iter_sections()) + + subtrees = tested._homogeneous_subtrees(axon_on_basal) + + assert subtrees[0].root_node.id == axon_on_basal.root_node.id + assert subtrees[0].root_node.type == NeuriteType.basal_dendrite + + assert subtrees[1].root_node.id == sections[4].id + assert subtrees[1].root_node.type == NeuriteType.axon + + +def test_iter_neurites__heterogeneous(mixed_morph): + + subtrees = list(tested.iter_neurites(mixed_morph, use_subtrees=False)) + + assert len(subtrees) == 3 + assert subtrees[0].type == NeuriteType.basal_dendrite + assert subtrees[1].type == NeuriteType.basal_dendrite + assert subtrees[2].type == NeuriteType.apical_dendrite + + subtrees = list(tested.iter_neurites(mixed_morph, use_subtrees=True)) + + assert len(subtrees) == 4 + assert subtrees[0].type == NeuriteType.basal_dendrite + assert subtrees[1].type == NeuriteType.basal_dendrite + assert subtrees[2].type == NeuriteType.axon + assert subtrees[3].type == NeuriteType.apical_dendrite + + @pytest.fixture def population(mixed_morph): return Population([mixed_morph, mixed_morph]) From 23570278760d13cf877d46adc70117ef6ac9c7bd Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 14 Apr 2022 15:56:00 +0200 Subject: [PATCH 75/87] Cleanup neurite features --- neurom/features/bifurcation.py | 33 +++++++++++--- neurom/features/neurite.py | 79 +++++++++++++--------------------- neurom/features/section.py | 7 +++ neurom/utils.py | 16 +++++++ 4 files changed, 81 insertions(+), 54 deletions(-) diff --git a/neurom/features/bifurcation.py b/neurom/features/bifurcation.py index 423a3ef2..3072db3a 100644 --- a/neurom/features/bifurcation.py +++ b/neurom/features/bifurcation.py @@ -33,7 +33,7 @@ from neurom.exceptions import NeuroMError from neurom.core.dataformat import COLS from neurom.core.morphology import Section -from neurom.features.section import section_mean_radius +from neurom.features import section as sf def _raise_if_not_bifurcation(section): @@ -156,8 +156,8 @@ def sibling_ratio(bif_point, method='first'): 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]) + n = sf.section_mean_radius(bif_point.children[0]) + m = sf.section_mean_radius(bif_point.children[1]) return min(n, m) / max(n, m) @@ -182,7 +182,28 @@ def diameter_power_relation(bif_point, method='first'): 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]) + d_child = sf.section_mean_radius(bif_point) + d_child1 = sf.section_mean_radius(bif_point.children[0]) + d_child2 = sf.section_mean_radius(bif_point.children[1]) return (d_child / d_child1)**(1.5) + (d_child / d_child2)**(1.5) + + +def downstream_pathlength_asymmetry( + bif_point, normalization_length=1.0, iterator_type=Section.ipreorder +): + """Calculates the downstream pathlength asymmetry at a bifurcation point. + + Args: + bif_point: Bifurcation section. + normalization_length: Constant to divide the result with. + iterator_type: Iterator type that specifies how the two subtrees are traversed. + + Returns: + The absolute difference between the downstream path distances of the two children, divided + by the normalization length. + """ + _raise_if_not_bifurcation(bif_point) + return abs( + sf.downstream_pathlength(bif_point.children[0], iterator_type=iterator_type) - + sf.downstream_pathlength(bif_point.children[1], iterator_type=iterator_type), + ) / normalization_length diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 1652cbfd..4884ff70 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -48,7 +48,7 @@ import numpy as np from neurom import morphmath -from neurom.utils import flatten +from neurom import utils from neurom.core.types import NeuriteType from neurom.core.morphology import Section, iter_points from neurom.core.dataformat import COLS @@ -69,6 +69,7 @@ def _map_sections(fun, neurite, iterator_type=Section.ipreorder, section_type=Ne def homogeneous_filter(section): return check_type(section) and Section.is_homogeneous_point(section) + # forking sections cannot be heterogeneous if ( iterator_type in {Section.ibifurcation_point, Section.iforking_point} and section_type != NeuriteType.all @@ -188,19 +189,14 @@ def section_term_branch_orders(neurite, section_type=NeuriteType.all): def section_path_distances(neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Path lengths.""" - def takeuntil(predicate, iterable): - """Similar to itertools.takewhile but it returns the last element before stopping.""" - for x in iterable: - yield x - if predicate(x): - break - - def pl2(node): + def path_length(node): """Calculate the path length using cached section lengths.""" - sections = takeuntil(lambda s: s.id == neurite.root_node.id, node.iupstream()) + sections = utils.takeuntil(lambda s: s.id == neurite.root_node.id, node.iupstream()) return sum(n.length for n in sections) - return _map_sections(pl2, neurite, iterator_type=iterator_type, section_type=section_type) + return _map_sections( + path_length, neurite, iterator_type=iterator_type, section_type=section_type + ) ################################################################################ @@ -213,7 +209,7 @@ def _map_segments(func, neurite, section_type=NeuriteType.all): `func` accepts a section and returns list of values corresponding to each segment. """ - return list(flatten(_map_sections(func, neurite, section_type=section_type))) + return list(utils.flatten(_map_sections(func, neurite, section_type=section_type))) @feature(shape=(...,)) @@ -293,14 +289,12 @@ def segments_path_length(section): @feature(shape=(...,)) def segment_radial_distances(neurite, origin=None, section_type=NeuriteType.all): """Returns the list of distances between all segment mid points and origin.""" - pos = neurite.root_node.points[0] if origin is None else origin - - def radial_distances(section): - """List of distances between the mid point of each segment and pos.""" - mid_pts = 0.5 * (section.points[:-1, COLS.XYZ] + section.points[1:, COLS.XYZ]) - return np.linalg.norm(mid_pts - pos[COLS.XYZ], axis=1) - - return _map_segments(radial_distances, neurite, section_type=section_type) + origin = neurite.root_node.points[0, COLS.XYZ] if origin is None else origin + return _map_segments( + func=partial(sf.segment_midpoint_radial_distances, origin=origin), + neurite=neurite, + section_type=section_type + ) @feature(shape=(...,)) @@ -337,44 +331,33 @@ def partition_asymmetry( :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}') + raise ValueError( + "Please provide a valid variant for partition asymmetry. " + f"Expected 'branch-order' or 'length', got {variant}." + ) if method not in {'petilla', 'uylings'}: - raise ValueError('Please provide a valid method for partition asymmetry,' - 'either "petilla" or "uylings"') - - def it_type(section): - - if section_type == NeuriteType.all: - return Section.ipreorder(section) + raise ValueError( + "Please provide a valid method for partition asymmetry. " + f"Expected 'petilla' or 'uylings', got {method}." + ) - check = is_type(section_type) - return (s for s in section.ipreorder() if check(s)) + # create a downstream iterator that is filtered by the section type + it_type = utils.filtered_iterator(is_type(section_type), Section.ipreorder) if variant == 'branch-order': - - function = partial( - bf.partition_asymmetry, uylings=method == 'uylings', iterator_type=it_type - ) - return _map_sections( - function, + partial(bf.partition_asymmetry, uylings=method == 'uylings', iterator_type=it_type), neurite, iterator_type=Section.ibifurcation_point, section_type=section_type ) - neurite_length = total_length(neurite, section_type=section_type) - - def pathlength_asymmetry_ratio(section): - pathlength_diff = abs( - sf.downstream_pathlength(section.children[0], iterator_type=it_type) - - sf.downstream_pathlength(section.children[1], iterator_type=it_type) - ) - return pathlength_diff / neurite_length - return _map_sections( - pathlength_asymmetry_ratio, + partial( + bf.downstream_pathlength_asymmetry, + normalization_length=total_length(neurite, section_type=section_type), + iterator_type=it_type, + ), neurite, iterator_type=Section.ibifurcation_point, section_type=section_type @@ -499,7 +482,7 @@ def get_points(section): # note: duplicate points included but not affect the convex hull calculation points = list( - flatten(_map_sections(get_points, neurite, section_type=section_type)) + utils.flatten(_map_sections(get_points, neurite, section_type=section_type)) ) hull = convex_hull(points) diff --git a/neurom/features/section.py b/neurom/features/section.py index 5eeb4129..6072e9ef 100644 --- a/neurom/features/section.py +++ b/neurom/features/section.py @@ -138,6 +138,13 @@ def segment_midpoints(section): return np.divide(np.add(pts[:-1], pts[1:]), 2.0).tolist() +def segment_midpoint_radial_distances(section, origin=None): + """Returns the list of segment midpoint radial distances to the origin.""" + origin = np.zeros(3, dtype=float) if origin is None else origin + midpoints = np.array(segment_midpoints(section)) + return np.linalg.norm(midpoints - origin, axis=1).tolist() + + def segment_taper_rates(section): """Returns the list of segment taper rates within the section.""" pts = section.points[:, COLS.XYZR] diff --git a/neurom/utils.py b/neurom/utils.py index 90ab2a4b..ef990cca 100644 --- a/neurom/utils.py +++ b/neurom/utils.py @@ -136,3 +136,19 @@ def str_to_plane(plane): def flatten(list_of_lists): """Flatten one level of nesting.""" return chain.from_iterable(list_of_lists) + + +def takeuntil(predicate, iterable): + """Similar to itertools.takewhile but it returns the last element before stopping.""" + for x in iterable: + yield x + if predicate(x): + break + + +def filtered_iterator(predicate, iterator_type): + """Returns an iterator function that is filtered by the predicate.""" + @wraps(iterator_type) + def composed(*args, **kwargs): + return filter(predicate, iterator_type(*args, **kwargs)) + return composed From 8bba54fc90af6d16880845848ccddbda58f5f284 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Sun, 17 Apr 2022 00:12:46 +0200 Subject: [PATCH 76/87] Add new feature, refactor, and more tests --- neurom/features/morphology.py | 8 +- neurom/features/neurite.py | 10 +-- neurom/features/section.py | 17 ++++- tests/test_mixed.py | 138 ++++++++++++++++++++++++++++++++-- 4 files changed, 154 insertions(+), 19 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index f22d2f67..b415f77a 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -762,7 +762,7 @@ def shape_factor(morph, neurite_type=NeuriteType.all, projection_plane="xy", use @feature(shape=()) -def length_fraction_above_soma(morph, neurite_type=NeuriteType.all, up="Y"): +def length_fraction_above_soma(morph, neurite_type=NeuriteType.all, up="Y", use_subtrees=False): """Returns the length fraction of the segments that have their midpoints higher than the soma. Args: @@ -779,7 +779,11 @@ def length_fraction_above_soma(morph, neurite_type=NeuriteType.all, up="Y"): raise NeuroMError(f"Unknown axis {axis}. Please choose 'X', 'Y', or 'Z'.") col = getattr(COLS, axis) - segments = list(iter_segments(morph, neurite_filter=is_type(neurite_type))) + + if use_subtrees: + segments = list(iter_segments(morph, neurite_filter=is_type(neurite_type))) + else: + segments = list(iter_segments(morph, section_filter=is_type(neurite_type))) if not segments: return np.nan diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 4884ff70..7c972779 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -188,14 +188,10 @@ def section_term_branch_orders(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) def section_path_distances(neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Path lengths.""" - - def path_length(node): - """Calculate the path length using cached section lengths.""" - sections = utils.takeuntil(lambda s: s.id == neurite.root_node.id, node.iupstream()) - return sum(n.length for n in sections) - return _map_sections( - path_length, neurite, iterator_type=iterator_type, section_type=section_type + partial(sf.section_path_length, stop_node=neurite.root_node), + neurite, + iterator_type=iterator_type, section_type=section_type ) diff --git a/neurom/features/section.py b/neurom/features/section.py index 6072e9ef..9809d495 100644 --- a/neurom/features/section.py +++ b/neurom/features/section.py @@ -35,6 +35,7 @@ from neurom.core.morphology import iter_segments from neurom.core.morphology import Section from neurom.morphmath import interval_lengths +from neurom import utils def section_points(section): @@ -42,9 +43,19 @@ def section_points(section): return section.points[:, COLS.XYZ] -def section_path_length(section): - """Path length from section to root.""" - return sum(s.length for s in section.iupstream()) +def section_path_length(section, stop_node=None): + """Path length from section to root. + + Args: + section: Section object. + stop_node: Node to stop the upstream traversal. If None, it stops when no parent is found. + """ + it = section.iupstream() + + if stop_node: + it = utils.takeuntil(lambda s: s.id == stop_node.id, it) + + return sum(map(section_length, it)) def section_length(section): diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 8522fab9..a9075500 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -10,15 +10,59 @@ from neurom.features import _POPULATION_FEATURES, _MORPHOLOGY_FEATURES, _NEURITE_FEATURES import collections.abc -from neurom.core import morphology as tested +from neurom.core.types import tree_type_checker as is_type + +import neurom.core.morphology +import neurom.features.neurite @pytest.fixture def mixed_morph(): """ + (1, 4, 1) + | + S7:B | + | + (1, 4, -1)-----(1, 4, 0) (2, 4, 0) (3, 3, 1) + S8:B | | | + | S10:A | S12:A | + | | S11:A | + S6:B | (2, 3, 0)-----(3, 3, 0) + | / | + | S9:A / S13:A | + | / | + (1, 2, 0) (3, 3, -1) + / + S5:B / + / Axon on basal dendrite + (-3, 0, 1) (-2, 1, 0) (0, 1, 0) + | | + S2 | S4 | + | S1 | S0 + (-3, 0, 0)-----(-2, 0, 0)-----(-1, 0, 0) (0, 0, 0) Soma + | + S3 | Basal Dendrite + | + (-3, 0, -1) (0, -1, 0) + | + S14 | + | S17 + Apical Dendrite (0, -2, 0)-----(1, -2, 0) + | + S15 | + S17 | S16 + (0, -3, -1)-----(0, -3, 0)-----(0, -3, 1) + basal_dendrite: homogeneous + section ids: [0, 1, 2, 3, 4] + axon_on_basal_dendrite: heterogeneous - apical_dendrite: homogeneous + section_ids: + - basal: [5, 6, 7, 8] + - axon : [9, 10, 11, 12, 13] + + apical_dendrite: homogeneous: + section_ids: [14, 15, 16, 17, 18] """ return neurom.load_morphology( """ @@ -73,11 +117,11 @@ def test_homogeneous_subtrees(mixed_morph): basal, axon_on_basal, apical = mixed_morph.neurites - assert tested._homogeneous_subtrees(basal) == [basal] + assert neurom.core.morphology._homogeneous_subtrees(basal) == [basal] sections = list(axon_on_basal.iter_sections()) - subtrees = tested._homogeneous_subtrees(axon_on_basal) + subtrees = neurom.core.morphology._homogeneous_subtrees(axon_on_basal) assert subtrees[0].root_node.id == axon_on_basal.root_node.id assert subtrees[0].root_node.type == NeuriteType.basal_dendrite @@ -88,14 +132,14 @@ def test_homogeneous_subtrees(mixed_morph): def test_iter_neurites__heterogeneous(mixed_morph): - subtrees = list(tested.iter_neurites(mixed_morph, use_subtrees=False)) + subtrees = list(neurom.core.morphology.iter_neurites(mixed_morph, use_subtrees=False)) assert len(subtrees) == 3 assert subtrees[0].type == NeuriteType.basal_dendrite assert subtrees[1].type == NeuriteType.basal_dendrite assert subtrees[2].type == NeuriteType.apical_dendrite - subtrees = list(tested.iter_neurites(mixed_morph, use_subtrees=True)) + subtrees = list(neurom.core.morphology.iter_neurites(mixed_morph, use_subtrees=True)) assert len(subtrees) == 4 assert subtrees[0].type == NeuriteType.basal_dendrite @@ -104,6 +148,74 @@ def test_iter_neurites__heterogeneous(mixed_morph): assert subtrees[3].type == NeuriteType.apical_dendrite +def test_core_iter_sections__heterogeneous(mixed_morph): + + def assert_sections(neurite, section_type, expected_section_ids): + + it = neurom.core.morphology.iter_sections(neurite, section_filter=is_type(section_type)) + assert [s.id for s in it] == expected_section_ids + + basal, axon_on_basal, apical = mixed_morph.neurites + + assert_sections(basal, NeuriteType.all, [0, 1, 2, 3, 4]) + assert_sections(basal, NeuriteType.basal_dendrite, [0, 1, 2, 3, 4]) + assert_sections(basal, NeuriteType.axon, []) + + assert_sections(axon_on_basal, NeuriteType.all, [5, 6, 7, 8, 9, 10, 11, 12, 13]) + assert_sections(axon_on_basal, NeuriteType.basal_dendrite, [5, 6, 7, 8]) + assert_sections(axon_on_basal, NeuriteType.axon, [9, 10, 11, 12, 13]) + + assert_sections(apical, NeuriteType.all, [14, 15, 16, 17, 18]) + assert_sections(apical, NeuriteType.apical_dendrite, [14, 15, 16, 17, 18]) + + +def test_features_neurite_map_sections__heterogeneous(mixed_morph): + + def assert_sections(neurite, section_type, iterator_type, expected_section_ids): + function = lambda section: section.id + section_ids = neurom.features.neurite._map_sections( + function, neurite, iterator_type=iterator_type, section_type=section_type + ) + assert section_ids == expected_section_ids + + basal, axon_on_basal, apical = mixed_morph.neurites + + # homogeneous tree, no difference between all and basal_dendrite types. + assert_sections( + basal, NeuriteType.all, neurom.core.morphology.Section.ibifurcation_point, + [0, 1], + ) + assert_sections( + basal, NeuriteType.basal_dendrite, neurom.core.morphology.Section.ibifurcation_point, + [0, 1], + ) + # heterogeneous tree, forks cannot be heterogeneous if a type other than all is specified + # Section with id 5 is the transition section, which has a basal and axon children sections + assert_sections( + axon_on_basal, NeuriteType.all, neurom.core.morphology.Section.ibifurcation_point, + [5, 6, 9, 11], + ) + assert_sections( + axon_on_basal, NeuriteType.basal_dendrite, + neurom.core.morphology.Section.ibifurcation_point, + [6], + ) + assert_sections( + axon_on_basal, NeuriteType.axon, + neurom.core.morphology.Section.ibifurcation_point, + [9, 11], + ) + # homogeneous tree, no difference between all and basal_dendrite types. + assert_sections( + apical, NeuriteType.all, neurom.core.morphology.Section.ibifurcation_point, + [14, 15], + ) + assert_sections( + apical, NeuriteType.apical_dendrite, neurom.core.morphology.Section.ibifurcation_point, + [14, 15], + ) + + @pytest.fixture def population(mixed_morph): return Population([mixed_morph, mixed_morph]) @@ -778,7 +890,19 @@ def _morphology_features(mode): "expected_wout_subtrees": 0.25, "expected_with_subtrees": 0.25, }, - ] + ], + "length_fraction_above_soma": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 0.567898, + "expected_with_subtrees": 0.567898, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 0.61591, + "expected_with_subtrees": 0.74729, + }, + ], } features_not_tested = set(_MORPHOLOGY_FEATURES) - set(features.keys()) From a3791b2f962ad9a8bf8f6b2eb1694aa523c4f0ce Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 18 Apr 2022 15:52:18 +0200 Subject: [PATCH 77/87] Refactor morphology.py --- neurom/features/morphology.py | 75 +++++++++++++---------------------- 1 file changed, 28 insertions(+), 47 deletions(-) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index b415f77a..c36525c2 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -45,7 +45,6 @@ import warnings -from itertools import chain from functools import partial from collections.abc import Iterable import math @@ -59,7 +58,7 @@ from neurom.core.dataformat import COLS from neurom.core.types import NeuriteType from neurom.exceptions import NeuroMError -from neurom.features import feature, NameSpace, neurite as nf +from neurom.features import feature, NameSpace, neurite as nf, section as sf from neurom.utils import str_to_plane from neurom.morphmath import convex_hull @@ -78,6 +77,11 @@ def _map_neurites(function, morph, neurite_type, use_subtrees=False): ) +def _map_neurite_root_nodes(function, morph, neurite_type, use_subtrees=False): + neurites = iter_neurites(obj=morph, filt=is_type(neurite_type), use_subtrees=use_subtrees) + return [function(neurite.root_node) for neurite in neurites] + + def _get_points(morph, neurite_type, use_subtrees=False): return list( iter_points(morph, section_filter=is_type(neurite_type)) @@ -153,13 +157,12 @@ def trunk_origin_azimuths(morph, neurite_type=NeuriteType.all): The range of the azimuth angle [-pi, pi] radians """ - def azimuth(neurite): + def azimuth(root_node): """Azimuth of a neurite trunk.""" return morphmath.azimuth_from_vector( - morphmath.vector(neurite.root_node.points[0], morph.soma.center) + morphmath.vector(root_node.points[0], morph.soma.center) ) - - return _map_neurites(azimuth, morph, neurite_type, use_subtrees=False) + return _map_neurite_root_nodes(azimuth, morph, neurite_type, use_subtrees=False) @feature(shape=(...,)) @@ -172,22 +175,22 @@ def trunk_origin_elevations(morph, neurite_type=NeuriteType.all): The range of the elevation angle [-pi/2, pi/2] radians """ - def elevation(neurite): + def elevation(root_node): """Elevation of a section.""" return morphmath.elevation_from_vector( - morphmath.vector(neurite.root_node.points[0], morph.soma.center) + morphmath.vector(root_node.points[0], morph.soma.center) ) - - return _map_neurites(elevation, morph, neurite_type, use_subtrees=False) + return _map_neurite_root_nodes(elevation, morph, neurite_type, use_subtrees=False) @feature(shape=(...,)) def trunk_vectors(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Calculate the vectors between all the trunks of the morphology and the soma center.""" - def vector_from_soma_to_root(neurite, *_, **__): - return morphmath.vector(neurite.root_node.points[0], morph.soma.center) - - return _map_neurites(vector_from_soma_to_root, morph, neurite_type, use_subtrees=use_subtrees) + def vector_from_soma_to_root(root_node): + return morphmath.vector(root_node.points[0], morph.soma.center) + return _map_neurite_root_nodes( + vector_from_soma_to_root, morph, neurite_type, use_subtrees=use_subtrees + ) @feature(shape=(...,)) @@ -407,9 +410,6 @@ def trunk_origin_radii( * else the mean radius of the points between the given ``min_length_filter`` and ``max_length_filter`` are returned. """ - def trunk_first_radius(neurite, *_, **__): - return neurite.root_node.points[0][COLS.R] - if min_length_filter is not None and min_length_filter <= 0: raise NeuroMError( "In 'trunk_origin_radii': the 'min_length_filter' value must be strictly greater " @@ -432,9 +432,12 @@ def trunk_first_radius(neurite, *_, **__): "'max_length_filter' value." ) - def trunk_mean_radius(neurite, *_, **__): + def trunk_first_radius(root_node): + return root_node.points[0][COLS.R] - points = neurite.root_node.points + def trunk_mean_radius(root_node): + + points = root_node.points interval_lengths = morphmath.interval_lengths(points) path_lengths = np.insert(np.cumsum(interval_lengths), 0, 0) @@ -470,24 +473,19 @@ def trunk_mean_radius(neurite, *_, **__): else trunk_mean_radius ) - return _map_neurites(function, morph, neurite_type, use_subtrees) + return _map_neurite_root_nodes(function, morph, neurite_type, use_subtrees=use_subtrees) @feature(shape=(...,)) def trunk_section_lengths(morph, neurite_type=NeuriteType.all, use_subtrees=False): """List of lengths of trunk sections of neurites in a morph.""" - def trunk_section_length(neurite, *_, **__): - return morphmath.section_length(neurite.root_node.points) - - return _map_neurites(trunk_section_length, morph, neurite_type, use_subtrees) + return _map_neurite_root_nodes(sf.section_length, morph, neurite_type, use_subtrees) @feature(shape=()) def number_of_neurites(morph, neurite_type=NeuriteType.all, use_subtrees=False): """Number of neurites in a morph.""" - def identity(n, *_, **__): - return n - return len(_map_neurites(identity, morph, neurite_type, use_subtrees)) + return len(_map_neurite_root_nodes(lambda n: n, morph, neurite_type, use_subtrees)) @feature(shape=(...,)) @@ -610,29 +608,12 @@ def _extent_along_axis(morph, axis, neurite_type, use_subtrees=False): The morphology is filtered by neurite type and the extent is calculated along the coordinate axis direction (e.g. COLS.X). """ - def iter_coordinates(neurite, section_type=NeuriteType.all): - return ( - coordinate - for section in iter_sections(neurite, section_filter=is_type(section_type)) - for coordinate in section.points[:, axis] - ) + points = _get_points(morph, neurite_type, use_subtrees=use_subtrees) - axis_coordinates = np.fromiter( - chain.from_iterable( - iter_neurites( - morph, - mapfun=iter_coordinates, - filt=is_type(neurite_type), - use_subtrees=use_subtrees - ) - ), - dtype=np.float32 - ) - - if len(axis_coordinates) == 0: + if not points: return 0.0 - return abs(np.ptp(axis_coordinates)) + return abs(np.ptp(np.asarray(points)[:, axis])) @feature(shape=()) From 44938b505c22487c54d3f48a20f88730bf937ec1 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 19 Apr 2022 16:00:27 +0200 Subject: [PATCH 78/87] Add documentation --- doc/source/heterogeneous.rst | 202 ++++++++++++++++++++ doc/source/images/heterogeneous_neurite.png | Bin 0 -> 8611 bytes doc/source/images/heterogeneous_neuron.png | Bin 0 -> 32564 bytes doc/source/index.rst | 1 + tests/data/swc/heterogeneous_morphology.swc | 25 +++ 5 files changed, 228 insertions(+) create mode 100644 doc/source/heterogeneous.rst create mode 100644 doc/source/images/heterogeneous_neurite.png create mode 100644 doc/source/images/heterogeneous_neuron.png create mode 100644 tests/data/swc/heterogeneous_morphology.swc diff --git a/doc/source/heterogeneous.rst b/doc/source/heterogeneous.rst new file mode 100644 index 00000000..8e2b2c5f --- /dev/null +++ b/doc/source/heterogeneous.rst @@ -0,0 +1,202 @@ +.. 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. + +.. _heterogeneous: + +Heterogeneous Morphologies +************************** + +.. image:: images/heterogeneous_neuron.png + +Definition +---------- + +A heterogeneous morphology consists of homogeneous and at least one heterogeneous neurite tree. A heterogeneous neurite tree consists of multiple sub-neurites with different types. + +A typical example of a heterogeneous neurite is the axon-carrying dendrite, in which the axon sprouts from the basal dendrite. + + +Identification +-------------- + +Heterogeneous neurites can be identified using the ``Neurite.is_heterogeneous`` method: + +.. code:: python + + from neurom import load_morphology + from neurom.core.morphology import iter_neurites + + m = load_morphology('tests/data/swc/heterogeneous_morphology.swc') + + print([neurite.is_heterogeneous() for neurite in m]) + +which would return ``[False, True, False]`` in this case. + + +Sub-neurite views of heterogeneous neurites +-------------------------------------------- + +Default mode +~~~~~~~~~~~~ + +NeuroM does not take into account heterogeneous sub-neurites by default. A heterogeneous neurite is treated as a homogeneous one, the type of which is determined by the first section of +the tree. For example: + +.. code-block:: python + + from neurom import load_morphology + from neurom.core.morphology import iter_neurites + + m = load_morphology('tests/data/swc/heterogeneous_morphology.swc') + + basal, axon_carrying_dendrite, apical = list(iter_neurites(m)) + + print(basal.type, axon_carrying_dendrite.type, apical.type) + +would print the types ``(basal_dendrite, basal_dendrite, apical_dendrite)``, i.e. the axon-carrying dendrite would be treated as a basal dendrite. From feature extraction to checks, the axon-carrying dendrite is treated as a basal dendrite. Features, for which an axon neurite type is passed, do not have access to the axonal part of the neurite. For instance, the number of basal and axon neurites will be two and zero respectively. + +Sub-neurite mode +~~~~~~~~~~~~~~~~ + +NeuroM provides an immutable approach (without modifying the morphology) to access the homogeneous sub-neurites of a neurite. Using ``iter_neurites`` with the flag ``use_subtrees`` activated returns a neurite view for each homogeneous sub-neurite. + +.. code-block:: python + + basal1, basal2, axon, apical = list(iter_neurites(m, use_subtrees=True)) + + print(basal1.type, basal2.type, axon.type, apical.type) + +In the example above, two views of the axon-carrying dendrite have been created: the basal and axon dendrite views. + +.. image:: images/heterogeneous_neurite.png + +Given that the morphology is not modified, the sub-neurites specify as their ``root_node`` the section of the homogeneous sub-neurite. They are just pointers to where the sub-neurites start. + +.. note:: + Creating neurite instances for the homogeneous sub-neurites breaks the assumption of root nodes not having a parent. + + +.. warning:: + Be careful while using sub-neurites. Because they just point to the start sections of the sub-neurite, they may include other sub-neurites as well. In the figure example above, the basal + sub-neurite includes the entire tree, including the axon sub-neurite. An additional filtering of the sections is needed to leave out the axonal part. However, for the axon sub-neurite this + filtering is not needed because it is downstream homogeneous. + + +Extract features from heterogeneous morphologies +------------------------------------------------ + +Neurite +~~~~~~~ + +Neurite features have been extended to include a ``section_type`` argument, which can be used to apply a feature on a heterogeneous neurite. + +.. code-block:: python + + from neurom import NeuriteType + from neurom import load_morphology + from neurom.features.neurite import number_of_sections + + m = load_morphology('tests/data/swc/heterogeneous_morphology.swc') + + axon_carrying_dendrite = m.neurites[1] + + total_sections = number_of_sections(axon_carrying_dendrite) + basal_sections = number_of_sections(axon_carrying_dendrite, section_type=NeuriteType.basal_dendrite) + axon_sections = number_of_sections(axon_carrying_dendrite, section_type=NeuriteType.axon) + + print(total_sections, basal_sections, axon_sections) + +Not specifying a ``section_type``, which is equivalent to passing ``NeuriteType.all``, will use all sections as done so far by NeuroM. + +Morphology +~~~~~~~~~~ + +Morphology features have been extended to include the ``use_subtrees`` flag, which allows to use the sub-neurites. + +.. code-block:: python + + from neurom import NeuriteType + from neurom import load_morphology + from neurom.features.morphology import number_of_neurites + + m = load_morphology('tests/data/swc/heterogeneous_morphology.swc') + + total_neurites_wout_subneurites = number_of_neurites(m) + total_neurites_with_subneurites = number_of_neurites(m, use_subtrees=True) + + print(total_neurites_wout_subneurites, total_neurites_with_subneurites) + + number_of_axon_neurites_wout = number_of_neurites(m, neurite_type=NeuriteType.axon) + number_of_axon_neurites_with = number_of_neurites(m, neurite_type=NeuriteType.axon, use_subtrees=True) + + print(number_of_axon_neurites_wout, number_of_axon_neurites_with) + + number_of_basal_neurites_wout = number_of_neurites(m, neurite_type=NeuriteType.basal_dendrite) + number_of_basal_neurites_with = number_of_neurites(m, neurite_type=NeuriteType.basal_dendrite, use_subtrees=True) + + print(number_of_basal_neurites_wout, number_of_basal_neurites_with) + +In the example above, the total number of neurites increases from 3 to 4 when the subtrees are enabled. This is because the axonal and basal parts of the axon-carrying dendrite are counted separately +in the second case. + +Specifying a ``neurite_type``, allows to count sub-neurites. Therefore, the number of axons without subtrees is 0, whereas it is 1 when subtrees are enabled. However, for basal dendrites the number +does not change (2) because the axon-carrying dendrite is perceived as basal dendrite in the default case. + +features.get +~~~~~~~~~~~~ + +``features.get`` can be used with respect to what has been mentioned above for neurite and morphology features. + +.. code-block:: python + + from neurom import features + from neurom import load_morphology + + m = load_morphology('tests/data/swc/heterogeneous_morphology.swc') + + features.get("number_of_neurites", m, use_subtrees=True) + features.get("number_of_sections", m, section_type=NeuriteType.axon) + +Conventions & Incompatibilities +------------------------------- + +Heterogeneous Forks +~~~~~~~~~~~~~~~~~~~ + +A heterogeneous bifurcation/fork, i.e. a section with children of different types, is ignored when features on bifurcations are calculated. It is not meaningful to calculate features, such as bifurcation angles, on transitional forks where the downstream subtrees have different types. + +Incompatible features with subtrees +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following features are not compatible with subtrees: + +* trunk_origin_azimuths +* trunk_origin_elevations +* trunk_angles + +because they require the neurites to be rooted at the soma. This is not true for sub-neurites. Therefore, passing a ``use_subtrees`` flag, will result to an error. diff --git a/doc/source/images/heterogeneous_neurite.png b/doc/source/images/heterogeneous_neurite.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4d08114e8d4bfd448bc936e05c45b08c02784c GIT binary patch literal 8611 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 Date: Tue, 19 Apr 2022 20:05:20 +0200 Subject: [PATCH 79/87] Generalize iupstream to also stop at node --- neurom/core/morphology.py | 25 +++++++++++++++++++------ neurom/features/section.py | 8 +------- neurom/utils.py | 8 -------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index f2438684..7e2747bc 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -116,12 +116,25 @@ def ipostorder(self): 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 iupstream(self, stop_node=None): + """Iterate from a tree node to the root nodes. + + Args: + stop_node: Node to stop the upstream traversal. If None, it stops when parent is None. + """ + def stop_if_no_parent(section): + return section.parent is None + + def stop_at_node(section): + return section == stop_node + + stop_condition = stop_if_no_parent if stop_node is None else stop_at_node + + current_section = self + while not stop_condition(current_section): + yield current_section + current_section = current_section.parent + yield current_section def ileaf(self): """Iterator to all leaves of a tree.""" diff --git a/neurom/features/section.py b/neurom/features/section.py index 9809d495..e9c5ed6d 100644 --- a/neurom/features/section.py +++ b/neurom/features/section.py @@ -35,7 +35,6 @@ from neurom.core.morphology import iter_segments from neurom.core.morphology import Section from neurom.morphmath import interval_lengths -from neurom import utils def section_points(section): @@ -50,12 +49,7 @@ def section_path_length(section, stop_node=None): section: Section object. stop_node: Node to stop the upstream traversal. If None, it stops when no parent is found. """ - it = section.iupstream() - - if stop_node: - it = utils.takeuntil(lambda s: s.id == stop_node.id, it) - - return sum(map(section_length, it)) + return sum(map(section_length, section.iupstream(stop_node=stop_node))) def section_length(section): diff --git a/neurom/utils.py b/neurom/utils.py index ef990cca..a2a04eab 100644 --- a/neurom/utils.py +++ b/neurom/utils.py @@ -138,14 +138,6 @@ def flatten(list_of_lists): return chain.from_iterable(list_of_lists) -def takeuntil(predicate, iterable): - """Similar to itertools.takewhile but it returns the last element before stopping.""" - for x in iterable: - yield x - if predicate(x): - break - - def filtered_iterator(predicate, iterator_type): """Returns an iterator function that is filtered by the predicate.""" @wraps(iterator_type) From cbe287a9d6b1dd089eb7b80917397c6f233c9cc5 Mon Sep 17 00:00:00 2001 From: Mike Gevaert Date: Thu, 21 Apr 2022 09:55:23 +0200 Subject: [PATCH 80/87] some doc reflow --- doc/source/heterogeneous.rst | 64 +++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/doc/source/heterogeneous.rst b/doc/source/heterogeneous.rst index 8e2b2c5f..3cd5731a 100644 --- a/doc/source/heterogeneous.rst +++ b/doc/source/heterogeneous.rst @@ -1,4 +1,4 @@ -.. Copyright (c) 2015, Ecole Polytechnique Federale de Lausanne, Blue Brain Project +.. Copyright (c) 2022, Ecole Polytechnique Federale de Lausanne, Blue Brain Project All rights reserved. This file is part of NeuroM @@ -36,7 +36,8 @@ Heterogeneous Morphologies Definition ---------- -A heterogeneous morphology consists of homogeneous and at least one heterogeneous neurite tree. A heterogeneous neurite tree consists of multiple sub-neurites with different types. +A heterogeneous morphology consists of zero or more homogeneous and at least one heterogeneous neurite trees extending from the soma. +A heterogeneous neurite tree consists of multiple sub-neurites with different types (ie: basal and axon). A typical example of a heterogeneous neurite is the axon-carrying dendrite, in which the axon sprouts from the basal dendrite. @@ -44,7 +45,7 @@ A typical example of a heterogeneous neurite is the axon-carrying dendrite, in w Identification -------------- -Heterogeneous neurites can be identified using the ``Neurite.is_heterogeneous`` method: +Heterogeneous neurites can be identified using the ``Neurite::is_heterogeneous`` method: .. code:: python @@ -55,7 +56,7 @@ Heterogeneous neurites can be identified using the ``Neurite.is_heterogeneous`` print([neurite.is_heterogeneous() for neurite in m]) -which would return ``[False, True, False]`` in this case. +which would return ``[False, True, False]``, meaning the 2nd neurite extending from the soma contains multiple neurite types. Sub-neurite views of heterogeneous neurites @@ -64,8 +65,9 @@ Sub-neurite views of heterogeneous neurites Default mode ~~~~~~~~~~~~ -NeuroM does not take into account heterogeneous sub-neurites by default. A heterogeneous neurite is treated as a homogeneous one, the type of which is determined by the first section of -the tree. For example: +NeuroM does not take into account heterogeneous sub-neurites by default. +A heterogeneous neurite is treated as a homogeneous one, the type of which is determined by the first section of the tree. +For example: .. code-block:: python @@ -78,12 +80,17 @@ the tree. For example: print(basal.type, axon_carrying_dendrite.type, apical.type) -would print the types ``(basal_dendrite, basal_dendrite, apical_dendrite)``, i.e. the axon-carrying dendrite would be treated as a basal dendrite. From feature extraction to checks, the axon-carrying dendrite is treated as a basal dendrite. Features, for which an axon neurite type is passed, do not have access to the axonal part of the neurite. For instance, the number of basal and axon neurites will be two and zero respectively. +would print the types ``(basal_dendrite, basal_dendrite, apical_dendrite)``. +I.E. the axon-carrying dendrite would be treated as a basal dendrite. +For feature extraction and checks, the axon-carrying dendrite is treated as a basal dendrite. +Features, for which an axon neurite type is passed, do not have access to the axonal part of the neurite. +For instance, the number of basal and axon neurites will be two and zero respectively. Sub-neurite mode ~~~~~~~~~~~~~~~~ -NeuroM provides an immutable approach (without modifying the morphology) to access the homogeneous sub-neurites of a neurite. Using ``iter_neurites`` with the flag ``use_subtrees`` activated returns a neurite view for each homogeneous sub-neurite. +NeuroM provides an immutable approach (without modifying the morphology) to access the homogeneous sub-neurites of a neurite. +Using ``iter_neurites`` with the flag ``use_subtrees`` returns a neurite view for each homogeneous sub-neurite. .. code-block:: python @@ -95,16 +102,19 @@ In the example above, two views of the axon-carrying dendrite have been created: .. image:: images/heterogeneous_neurite.png -Given that the morphology is not modified, the sub-neurites specify as their ``root_node`` the section of the homogeneous sub-neurite. They are just pointers to where the sub-neurites start. +Given that the morphology is not modified, the sub-neurites specify as their ``root_node`` the section of the homogeneous sub-neurite. +They are just references to where the sub-neurites start. .. note:: Creating neurite instances for the homogeneous sub-neurites breaks the assumption of root nodes not having a parent. .. warning:: - Be careful while using sub-neurites. Because they just point to the start sections of the sub-neurite, they may include other sub-neurites as well. In the figure example above, the basal - sub-neurite includes the entire tree, including the axon sub-neurite. An additional filtering of the sections is needed to leave out the axonal part. However, for the axon sub-neurite this - filtering is not needed because it is downstream homogeneous. + Be careful while using sub-neurites. + Because they just point to the start sections of the sub-neurite, they may include other sub-neurites as well. + In the figure example above, the basal sub-neurite includes the entire tree, including the axon sub-neurite. + An additional filtering of the sections is needed to leave out the axonal part. + However, for the axon sub-neurite this filtering is not needed because it is downstream homogeneous. Extract features from heterogeneous morphologies @@ -131,7 +141,7 @@ Neurite features have been extended to include a ``section_type`` argument, whic print(total_sections, basal_sections, axon_sections) -Not specifying a ``section_type``, which is equivalent to passing ``NeuriteType.all``, will use all sections as done so far by NeuroM. +Not specifying a ``section_type`` is equivalent to passing ``NeuriteType.all`` and it will use all sections as done historically. Morphology ~~~~~~~~~~ @@ -149,23 +159,30 @@ Morphology features have been extended to include the ``use_subtrees`` flag, whi total_neurites_wout_subneurites = number_of_neurites(m) total_neurites_with_subneurites = number_of_neurites(m, use_subtrees=True) - print(total_neurites_wout_subneurites, total_neurites_with_subneurites) + print("A:", total_neurites_wout_subneurites, total_neurites_with_subneurites) number_of_axon_neurites_wout = number_of_neurites(m, neurite_type=NeuriteType.axon) number_of_axon_neurites_with = number_of_neurites(m, neurite_type=NeuriteType.axon, use_subtrees=True) - print(number_of_axon_neurites_wout, number_of_axon_neurites_with) + print("B:", number_of_axon_neurites_wout, number_of_axon_neurites_with) number_of_basal_neurites_wout = number_of_neurites(m, neurite_type=NeuriteType.basal_dendrite) number_of_basal_neurites_with = number_of_neurites(m, neurite_type=NeuriteType.basal_dendrite, use_subtrees=True) - print(number_of_basal_neurites_wout, number_of_basal_neurites_with) + print("C:", number_of_basal_neurites_wout, number_of_basal_neurites_with) -In the example above, the total number of neurites increases from 3 to 4 when the subtrees are enabled. This is because the axonal and basal parts of the axon-carrying dendrite are counted separately -in the second case. +Prints:: -Specifying a ``neurite_type``, allows to count sub-neurites. Therefore, the number of axons without subtrees is 0, whereas it is 1 when subtrees are enabled. However, for basal dendrites the number -does not change (2) because the axon-carrying dendrite is perceived as basal dendrite in the default case. + A: 3 4 + B: 0 1 + C: 2 2 + +In the example above, the total number of neurites increases from 3 to 4 when the subtrees are enabled (see ``A`` in the print out.) +This is because the axonal and basal parts of the axon-carrying dendrite are counted separately in the second case. + +Specifying a ``neurite_type``, allows to count sub-neurites. +Therefore, the number of axons without subtrees is 0, whereas it is 1 when subtrees are enabled (see ``B`` in the print out.) +However, for basal dendrites the number does not change (2) because the axon-carrying dendrite is perceived as basal dendrite in the default case (see ``C``.) features.get ~~~~~~~~~~~~ @@ -188,7 +205,8 @@ Conventions & Incompatibilities Heterogeneous Forks ~~~~~~~~~~~~~~~~~~~ -A heterogeneous bifurcation/fork, i.e. a section with children of different types, is ignored when features on bifurcations are calculated. It is not meaningful to calculate features, such as bifurcation angles, on transitional forks where the downstream subtrees have different types. +A heterogeneous bifurcation/fork, i.e. a section with children of different types, is ignored when features on bifurcations are calculated. +It is not meaningful to calculate features, such as bifurcation angles, on transitional forks where the downstream subtrees have different types. Incompatible features with subtrees ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -199,4 +217,6 @@ The following features are not compatible with subtrees: * trunk_origin_elevations * trunk_angles -because they require the neurites to be rooted at the soma. This is not true for sub-neurites. Therefore, passing a ``use_subtrees`` flag, will result to an error. +Because they require the neurites to be rooted at the soma. +This is not true for sub-neurites. +Therefore, passing a ``use_subtrees`` flag will result in an error. From 94a6a8c2beedd408adb035e3f411a908c09a88c0 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 21 Apr 2022 10:27:51 +0200 Subject: [PATCH 81/87] Simplify iupstream --- neurom/core/morphology.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 7e2747bc..916cba5d 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -122,13 +122,12 @@ def iupstream(self, stop_node=None): Args: stop_node: Node to stop the upstream traversal. If None, it stops when parent is None. """ - def stop_if_no_parent(section): - return section.parent is None - - def stop_at_node(section): - return section == stop_node - - stop_condition = stop_if_no_parent if stop_node is None else stop_at_node + if stop_node is None: + def stop_condition(section): + return section.parent is None + else: + def stop_condition(section): + return section == stop_node current_section = self while not stop_condition(current_section): From 1848f1d9be20496edaf3bb27fee7ca9ff49fbe54 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 21 Apr 2022 11:13:13 +0200 Subject: [PATCH 82/87] Add brief description for total_volume for default mode --- doc/source/heterogeneous.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/source/heterogeneous.rst b/doc/source/heterogeneous.rst index 3cd5731a..85e014a2 100644 --- a/doc/source/heterogeneous.rst +++ b/doc/source/heterogeneous.rst @@ -80,11 +80,15 @@ For example: print(basal.type, axon_carrying_dendrite.type, apical.type) -would print the types ``(basal_dendrite, basal_dendrite, apical_dendrite)``. +Prints:: + + NeuriteType.basal_dendrite NeuriteType.basal_dendrite NeuriteType.apical_dendrite + I.E. the axon-carrying dendrite would be treated as a basal dendrite. For feature extraction and checks, the axon-carrying dendrite is treated as a basal dendrite. Features, for which an axon neurite type is passed, do not have access to the axonal part of the neurite. For instance, the number of basal and axon neurites will be two and zero respectively. +A features such as ``total_volume`` would include the entire axon-carrying dendrite, without separating between basal and axon types. Sub-neurite mode ~~~~~~~~~~~~~~~~ From baded710aea7c9f5d04edce7c1fa0f3e67ac8a07 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 21 Apr 2022 20:10:04 +0200 Subject: [PATCH 83/87] Make neurites iterable readable --- neurom/core/morphology.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 916cba5d..c0e731c8 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -271,13 +271,12 @@ def iter_neurites( >>> 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 isinstance(obj, Neurite): + neurites = (obj,) + elif hasattr(obj, "neurites"): + neurites = obj.neurites + else: + neurites = obj if neurite_order == NeuriteIter.NRN: if isinstance(obj, Population): From 15aecdb0f9a8ba943451b9c7f04ef74886175b8f Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 21 Apr 2022 20:11:14 +0200 Subject: [PATCH 84/87] Drop extra line --- neurom/core/morphology.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index c0e731c8..ef80cf57 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -286,7 +286,6 @@ def iter_neurites( neurites = sorted(neurites, key=lambda neurite: NRN_ORDER.get(neurite.type, last_position)) if use_subtrees: - neurites = flatten( _homogeneous_subtrees(neurite) if neurite.is_heterogeneous() else [neurite] for neurite in neurites From cd21427a4c2de69f4278dbd8519b4e162046bcc0 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Thu, 21 Apr 2022 20:25:06 +0200 Subject: [PATCH 85/87] Use full path of section features --- neurom/features/bifurcation.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/neurom/features/bifurcation.py b/neurom/features/bifurcation.py index 3072db3a..0bc25479 100644 --- a/neurom/features/bifurcation.py +++ b/neurom/features/bifurcation.py @@ -29,11 +29,12 @@ """Bifurcation point functions.""" import numpy as np + +import neurom.features.section from neurom import morphmath -from neurom.exceptions import NeuroMError from neurom.core.dataformat import COLS from neurom.core.morphology import Section -from neurom.features import section as sf +from neurom.exceptions import NeuroMError def _raise_if_not_bifurcation(section): @@ -156,8 +157,8 @@ def sibling_ratio(bif_point, method='first'): n = bif_point.children[0].points[1, COLS.R] m = bif_point.children[1].points[1, COLS.R] if method == 'mean': - n = sf.section_mean_radius(bif_point.children[0]) - m = sf.section_mean_radius(bif_point.children[1]) + n = neurom.features.section.section_mean_radius(bif_point.children[0]) + m = neurom.features.section.section_mean_radius(bif_point.children[1]) return min(n, m) / max(n, m) @@ -182,9 +183,9 @@ def diameter_power_relation(bif_point, method='first'): 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 = sf.section_mean_radius(bif_point) - d_child1 = sf.section_mean_radius(bif_point.children[0]) - d_child2 = sf.section_mean_radius(bif_point.children[1]) + d_child = neurom.features.section.section_mean_radius(bif_point) + d_child1 = neurom.features.section.section_mean_radius(bif_point.children[0]) + d_child2 = neurom.features.section.section_mean_radius(bif_point.children[1]) return (d_child / d_child1)**(1.5) + (d_child / d_child2)**(1.5) @@ -203,7 +204,14 @@ def downstream_pathlength_asymmetry( by the normalization length. """ _raise_if_not_bifurcation(bif_point) - return abs( - sf.downstream_pathlength(bif_point.children[0], iterator_type=iterator_type) - - sf.downstream_pathlength(bif_point.children[1], iterator_type=iterator_type), - ) / normalization_length + return ( + abs( + neurom.features.section.downstream_pathlength( + bif_point.children[0], iterator_type=iterator_type + ) + - neurom.features.section.downstream_pathlength( + bif_point.children[1], iterator_type=iterator_type + ), + ) + / normalization_length + ) From cd70e9090b29e028158f049b3740d426e2ba425a Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 22 Apr 2022 10:25:48 +0200 Subject: [PATCH 86/87] Add warning for non AcD neurites --- neurom/core/morphology.py | 10 ++++++++++ tests/test_mixed.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index ef80cf57..9eae5acc 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -237,6 +237,16 @@ def _homogeneous_subtrees(neurite): for section in neurite.root_node.ipreorder(): if section.type not in homogeneous_neurites: homogeneous_neurites[section.type] = Neurite(section.morphio_section) + + if len(homogeneous_neurites) >= 2 and homogeneous_neurites.keys() != { + NeuriteType.axon, + NeuriteType.basal_dendrite, + }: + warnings.warn( + f"{neurite} is not an axon-carrying dendrite. " + f"Types found {list(homogeneous_neurites.keys())}", + stacklevel=2 + ) return list(homogeneous_neurites.values()) diff --git a/tests/test_mixed.py b/tests/test_mixed.py index a9075500..9cb11822 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -92,8 +92,25 @@ def mixed_morph(): """, reader="swc") +@pytest.fixture +def three_types_neurite_morph(): + return neurom.load_morphology( + """ + 1 1 0 0 0 0.5 -1 + 2 3 0 1 0 0.1 1 + 3 3 1 2 0 0.1 2 + 4 3 1 4 0 0.1 3 + 5 3 1 4 1 0.1 4 + 6 3 1 4 -1 0.1 4 + 7 2 2 3 0 0.1 3 + 8 2 2 4 0 0.1 7 + 9 2 3 3 0 0.1 7 + 10 2 3 3 1 0.1 9 + 11 4 3 3 -1 0.1 9 + """, + reader="swc") -def test_heterogeneous_neurite(mixed_morph): +def test_heterogeneous_neurites(mixed_morph): assert not mixed_morph.neurites[0].is_heterogeneous() assert mixed_morph.neurites[1].is_heterogeneous() @@ -113,7 +130,7 @@ def test_is_homogeneous_point(mixed_morph): assert sections[1].is_homogeneous_point() -def test_homogeneous_subtrees(mixed_morph): +def test_homogeneous_subtrees(mixed_morph, three_types_neurite_morph): basal, axon_on_basal, apical = mixed_morph.neurites @@ -129,6 +146,13 @@ def test_homogeneous_subtrees(mixed_morph): assert subtrees[1].root_node.id == sections[4].id assert subtrees[1].root_node.type == NeuriteType.axon + with pytest.warns( + UserWarning, + match="Neurite is not an axon-carrying dendrite." + ): + three_types_neurite, = three_types_neurite_morph.neurites + neurom.core.morphology._homogeneous_subtrees(three_types_neurite) + def test_iter_neurites__heterogeneous(mixed_morph): From d427225aa85662a7c79f324498b01389080492a7 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Fri, 22 Apr 2022 12:10:21 +0200 Subject: [PATCH 87/87] Do not use a set for subtrees to allow finding many of same type. --- neurom/core/morphology.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 9eae5acc..f5b2146b 100644 --- a/neurom/core/morphology.py +++ b/neurom/core/morphology.py @@ -233,21 +233,25 @@ def _homogeneous_subtrees(neurite): A sub-neurite can be either the entire tree or a homogeneous downstream sub-tree. """ - homogeneous_neurites = {neurite.root_node.type: neurite} - for section in neurite.root_node.ipreorder(): - if section.type not in homogeneous_neurites: - homogeneous_neurites[section.type] = Neurite(section.morphio_section) + it = neurite.root_node.ipreorder() + homogeneous_neurites = [Neurite(next(it).morphio_section)] - if len(homogeneous_neurites) >= 2 and homogeneous_neurites.keys() != { + for section in it: + if section.type != section.parent.type: + homogeneous_neurites.append(Neurite(section.morphio_section)) + + homogeneous_types = [neurite.type for neurite in homogeneous_neurites] + + if len(homogeneous_neurites) >= 2 and homogeneous_types != [ NeuriteType.axon, NeuriteType.basal_dendrite, - }: + ]: warnings.warn( f"{neurite} is not an axon-carrying dendrite. " - f"Types found {list(homogeneous_neurites.keys())}", + f"Subtree types found {homogeneous_types}", stacklevel=2 ) - return list(homogeneous_neurites.values()) + return homogeneous_neurites def iter_neurites(