diff --git a/doc/source/heterogeneous.rst b/doc/source/heterogeneous.rst new file mode 100644 index 00000000..85e014a2 --- /dev/null +++ b/doc/source/heterogeneous.rst @@ -0,0 +1,226 @@ +.. Copyright (c) 2022, 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 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. + + +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]``, meaning the 2nd neurite extending from the soma contains multiple neurite types. + + +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) + +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 +~~~~~~~~~~~~~~~~ + +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 + + 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 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. + + +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`` is equivalent to passing ``NeuriteType.all`` and it will use all sections as done historically. + +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("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("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("C:", number_of_basal_neurites_wout, number_of_basal_neurites_with) + +Prints:: + + 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 +~~~~~~~~~~~~ + +``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 in an error. diff --git a/doc/source/images/heterogeneous_neurite.png b/doc/source/images/heterogeneous_neurite.png new file mode 100644 index 00000000..ed4d0811 Binary files /dev/null and b/doc/source/images/heterogeneous_neurite.png differ diff --git a/doc/source/images/heterogeneous_neuron.png b/doc/source/images/heterogeneous_neuron.png new file mode 100644 index 00000000..c886410f Binary files /dev/null and b/doc/source/images/heterogeneous_neuron.png differ diff --git a/doc/source/index.rst b/doc/source/index.rst index a8aee503..b0d1d960 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -46,6 +46,7 @@ NeuroM is a Python-based toolkit for the analysis and processing of morphologies features spherical_coordinates examples + heterogeneous cli definitions api diff --git a/neurom/core/morphology.py b/neurom/core/morphology.py index 1a8404a8..f5b2146b 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 @@ -112,12 +116,24 @@ 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. + """ + 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): + yield current_section + current_section = current_section.parent + yield current_section def ileaf(self): """Iterator to all leaves of a tree.""" @@ -211,7 +227,36 @@ def __repr__(self): NeuriteType.undefined: 4} -def iter_neurites(obj, mapfun=None, filt=None, neurite_order=NeuriteIter.FileOrder): +def _homogeneous_subtrees(neurite): + """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. + """ + it = neurite.root_node.ipreorder() + homogeneous_neurites = [Neurite(next(it).morphio_section)] + + 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"Subtree types found {homogeneous_types}", + stacklevel=2 + ) + return homogeneous_neurites + + +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 +285,13 @@ 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) + 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): warnings.warn('`iter_neurites` with `neurite_order` over Population orders neurites' @@ -249,14 +299,28 @@ 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)) + if use_subtrees: + 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) - 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, 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: @@ -272,6 +336,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: @@ -282,13 +348,14 @@ 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) - ) + 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) -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: @@ -297,6 +364,7 @@ def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder) 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 @@ -306,7 +374,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:]) @@ -314,6 +383,35 @@ def iter_segments(obj, neurite_filter=None, neurite_order=NeuriteIter.FileOrder) ) +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) @@ -368,7 +466,9 @@ 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()) + # pylint: disable=import-outside-toplevel + from neurom.features.neurite import total_length + return total_length(self) @property def area(self): @@ -376,7 +476,9 @@ 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()) + # pylint: disable=import-outside-toplevel + from neurom.features.neurite import total_area + return total_area(self) @property def volume(self): @@ -384,7 +486,13 @@ 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.""" + return self.morphio_root_node.is_heterogeneous() def iter_sections(self, order=Section.ipreorder, neurite_order=NeuriteIter.FileOrder): """Iteration over section nodes. diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 924de8d1..65356970 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -36,9 +36,11 @@ >>> 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 +from functools import reduce, partial from neurom.core import Population, Morphology, Neurite from neurom.core.morphology import iter_neurites @@ -64,15 +66,30 @@ 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, 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 - 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 _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, **kwargs): +def _get_feature_value_and_func(feature_name, obj, use_subtrees=False, **kwargs): """Obtain a feature from a set of morphology objects. Arguments: @@ -87,43 +104,72 @@ def _get_feature_value_and_func(feature_name, obj, **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 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] + + if _is_subtree_processing_applicable(feature_): + 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) + 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 if feature_name in _POPULATION_FEATURES: feature_ = _POPULATION_FEATURES[feature_name] + + 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 _is_subtree_processing_applicable(feature_): + 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] 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, use_subtrees, **kwargs) + for n in obj + ] + ) if res is None or feature_ is None: raise NeuroMError(f'Cant apply "{feature_name}" feature. Please check that it exists, ' @@ -132,7 +178,7 @@ def _get_feature_value_and_func(feature_name, obj, **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 +193,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): diff --git a/neurom/features/bifurcation.py b/neurom/features/bifurcation.py index 423a3ef2..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.section import section_mean_radius +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 = section_mean_radius(bif_point.children[0]) - m = 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,7 +183,35 @@ 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 = 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) + + +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( + 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 + ) diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index ddee3aa1..c36525c2 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -46,11 +46,14 @@ import warnings from functools import partial +from collections.abc import Iterable 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, 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 @@ -63,9 +66,27 @@ feature = partial(feature, namespace=NameSpace.NEURON) -def _map_neurites(function, morph, neurite_type): +def _map_neurites(function, morph, neurite_type, use_subtrees=False): return list( - iter_neurites(morph, mapfun=function, filt=is_type(neurite_type)) + iter_neurites( + obj=morph, + mapfun=function, + filt=is_type(neurite_type), + use_subtrees=use_subtrees, + ) + ) + + +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)) + if use_subtrees + else iter_points(morph, neurite_filter=is_type(neurite_type)) ) @@ -92,35 +113,39 @@ 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 + 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. @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 _map_neurites(nf.number_of_sections, morph, neurite_type, use_subtrees) @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 _map_neurites(nf.total_length, morph, neurite_type, use_subtrees) @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 _map_neurites(nf.total_area, morph, neurite_type, use_subtrees) @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 _map_neurites(nf.total_volume, morph, neurite_type, use_subtrees) @feature(shape=(...,)) @@ -132,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) + return _map_neurite_root_nodes(azimuth, morph, neurite_type, use_subtrees=False) @feature(shape=(...,)) @@ -151,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) + return _map_neurite_root_nodes(elevation, morph, neurite_type, use_subtrees=False) @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) + 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=(...,)) @@ -247,6 +271,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. @@ -274,8 +299,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: @@ -310,6 +339,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. @@ -329,7 +359,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: @@ -357,6 +387,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. @@ -379,10 +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. """ - 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))] - 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 " @@ -405,11 +432,17 @@ def trunk_origin_radii( "'max_length_filter' value." ) - def _mean_radius(neurite): - points = neurite.root_node.points + def trunk_first_radius(root_node): + return root_node.points[0][COLS.R] + + 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) 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(): @@ -419,6 +452,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) @@ -430,34 +464,40 @@ def _mean_radius(neurite): ) # 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_neurite_root_nodes(function, morph, neurite_type, use_subtrees=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 _map_neurite_root_nodes(sf.section_length, 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_neurite_root_nodes(lambda n: n, morph, neurite_type, use_subtrees)) @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 _map_neurites(nf.volume_density, morph, neurite_type, use_subtrees) @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: @@ -478,11 +518,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)) @@ -499,13 +539,28 @@ 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 isinstance(morph, Iterable): + sections = filter(is_type(neurite_type), morph) + else: + 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: @@ -514,6 +569,7 @@ def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None 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, @@ -525,62 +581,61 @@ 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): +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] - ) - 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 + points = _get_points(morph, neurite_type, use_subtrees=use_subtrees) + + if not points: return 0.0 + return abs(np.ptp(np.asarray(points)[:, axis])) + @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) + 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 @@ -589,12 +644,7 @@ 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. """ - # 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 - ] + points = _get_points(morph, neurite_type, use_subtrees) if not points: return np.nan @@ -604,12 +654,14 @@ 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 -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())) @@ -623,9 +675,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) @@ -634,23 +684,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 @@ -661,16 +712,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 @@ -681,16 +733,17 @@ 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) @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: @@ -707,7 +760,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 6aac5e9f..7c972779 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -48,124 +48,151 @@ import numpy as np from neurom import morphmath -from neurom.core.morphology import Section +from neurom import utils +from neurom.core.types import NeuriteType +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 +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))) + check_type = is_type(section_type) + + 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 + ): + filt = homogeneous_filter + else: + filt = check_type + + return list(map(fun, filter(filt, iterator_type(neurite.root_node)))) @feature(shape=()) -def max_radial_distance(neurite): +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) + 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. @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)) + return sum(_map_sections(sf.number_of_segments, neurite, section_type=section_type)) @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=()) -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=()) -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=()) -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=()) -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=()) -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=()) -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)) @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(sf.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(sf.section_length, neurite, Section.ileaf, section_type) @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=(...,)) -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=(...,)) -def section_path_distances(neurite): +def section_path_distances(neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Path lengths.""" - - def pl2(node): - """Calculate the path length using cached section lengths.""" - return sum(n.length for n in node.iupstream()) - - return _map_sections(pl2, neurite) + return _map_sections( + partial(sf.section_path_length, stop_node=neurite.root_node), + neurite, + iterator_type=iterator_type, section_type=section_type + ) ################################################################################ @@ -173,120 +200,125 @@ 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. """ - return [ - segment_value - for section in Section.ipreorder(neurite.root_node) - for segment_value in func(section) - ] + return list(utils.flatten(_map_sections(func, neurite, section_type=section_type))) @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=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_mean_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=(...,)) -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=(...,)) -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=(...,)) -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 - - 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) + 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=(...,)) -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=(...,)) -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 @@ -295,46 +327,58 @@ def partition_asymmetry(neurite, variant='branch-order', method='petilla'): :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"') + raise ValueError( + "Please provide a valid method for partition asymmetry. " + f"Expected 'petilla' or 'uylings', got {method}." + ) + + # 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': return _map_sections( - partial(bf.partition_asymmetry, uylings=method == 'uylings'), + partial(bf.partition_asymmetry, uylings=method == 'uylings', iterator_type=it_type), neurite, - Section.ibifurcation_point) + iterator_type=Section.ibifurcation_point, + 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 + return _map_sections( + 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 + ) @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=(...,)) -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 @@ -342,25 +386,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 @@ -368,13 +415,18 @@ 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=(...,)) -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) @@ -383,29 +435,32 @@ 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=(...,)) -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=(...,)) -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=()) -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 @@ -416,46 +471,59 @@ 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 + neurite_volume = total_volume(neurite, section_type=section_type) + + def get_points(section): + return section.points[:, COLS.XYZ].tolist() + + # note: duplicate points included but not affect the convex hull calculation + points = list( + utils.flatten(_map_sections(get_points, neurite, section_type=section_type)) + ) + + hull = convex_hull(points) + + return neurite_volume / hull.volume if hull is not None else np.nan @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=(...,)) -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]] + points = list(iter_points(neurite, section_filter=is_type(section_type))) + + return [morphmath.principal_direction_extent(np.unique(points, axis=0))[direction]] @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/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/neurom/features/section.py b/neurom/features/section.py index 259699f3..e9c5ed6d 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 @@ -41,9 +42,14 @@ 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. + """ + return sum(map(section_length, section.iupstream(stop_node=stop_node))) def section_length(section): @@ -137,6 +143,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] @@ -213,6 +226,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/neurom/utils.py b/neurom/utils.py index 90ab2a4b..a2a04eab 100644 --- a/neurom/utils.py +++ b/neurom/utils.py @@ -136,3 +136,11 @@ def str_to_plane(plane): def flatten(list_of_lists): """Flatten one level of nesting.""" return chain.from_iterable(list_of_lists) + + +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 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')) diff --git a/tests/data/swc/heterogeneous_morphology.swc b/tests/data/swc/heterogeneous_morphology.swc new file mode 100644 index 00000000..d3b26ba5 --- /dev/null +++ b/tests/data/swc/heterogeneous_morphology.swc @@ -0,0 +1,25 @@ +# Created by MorphIO v3.3.3 +# index type X Y Z radius parent +1 1 0.000000000 0.000000000 0.000000000 0.500000000 -1 +2 3 -1.000000000 0.000000000 0.000000000 0.100000001 1 +3 3 -2.000000000 0.000000000 0.000000000 0.100000001 2 +4 3 -3.000000000 0.000000000 0.000000000 0.100000001 3 +5 3 -3.000000000 0.000000000 1.000000000 0.100000001 4 +6 3 -3.000000000 0.000000000 -1.000000000 0.100000001 4 +7 3 -2.000000000 1.000000000 0.000000000 0.100000001 3 +8 3 0.000000000 1.000000000 0.000000000 0.100000001 1 +9 3 1.000000000 2.000000000 0.000000000 0.100000001 8 +10 3 1.000000000 4.000000000 0.000000000 0.100000001 9 +11 3 1.000000000 4.000000000 1.000000000 0.100000001 10 +12 3 1.000000000 4.000000000 -1.000000000 0.100000001 10 +13 2 2.000000000 3.000000000 0.000000000 0.100000001 9 +14 2 2.000000000 4.000000000 0.000000000 0.100000001 13 +15 2 3.000000000 3.000000000 0.000000000 0.100000001 13 +16 2 3.000000000 3.000000000 1.000000000 0.100000001 15 +17 2 3.000000000 3.000000000 -1.000000000 0.100000001 15 +18 4 0.000000000 -1.000000000 0.000000000 0.100000001 1 +19 4 0.000000000 -2.000000000 0.000000000 0.100000001 18 +20 4 0.000000000 -3.000000000 0.000000000 0.100000001 19 +21 4 0.000000000 -3.000000000 1.000000000 0.100000001 20 +22 4 0.000000000 -3.000000000 -1.000000000 0.100000001 20 +23 4 1.000000000 -2.000000000 0.000000000 0.100000001 19 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(): 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)) diff --git a/tests/test_mixed.py b/tests/test_mixed.py new file mode 100644 index 00000000..9cb11822 --- /dev/null +++ b/tests/test_mixed.py @@ -0,0 +1,2157 @@ +import sys +import warnings +import pytest +import neurom +import numpy as np +import numpy.testing as npt +from neurom import NeuriteType +from neurom.features import get +from neurom.core import Population +from neurom.features import _POPULATION_FEATURES, _MORPHOLOGY_FEATURES, _NEURITE_FEATURES +import collections.abc + +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 + 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( + """ + 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") + +@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_neurites(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, three_types_neurite_morph): + + basal, axon_on_basal, apical = mixed_morph.neurites + + assert neurom.core.morphology._homogeneous_subtrees(basal) == [basal] + + sections = list(axon_on_basal.iter_sections()) + + 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 + + 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): + + 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(neurom.core.morphology.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 + + +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]) + + +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): + 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 _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 = { + "soma_radius": [ + { + "expected_wout_subtrees": 0.5, + "expected_with_subtrees": 0.5, + } + ], + "soma_surface_area": [ + { + "expected_wout_subtrees": np.pi, + "expected_with_subtrees": np.pi, + } + ], + "soma_volume": [ + { + "expected_wout_subtrees": np.pi / 6., + "expected_with_subtrees": np.pi / 6., + } + ], + "number_of_sections_per_neurite": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [5, 9, 5], + "expected_with_subtrees": [5, 4, 5, 5], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [5, 9], + "expected_with_subtrees": [5, 4], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [5], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [5], + "expected_with_subtrees": [5], + } + ], + "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 + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 3.741657, + "expected_with_subtrees": 3.316625, + }, + { + # with a global origin, AoD axon subtree [2, 4] is always furthest from soma + "kwargs": {"neurite_type": NeuriteType.all, "origin": np.array([0., 0., 0.])}, + "expected_wout_subtrees": 4.47213595499958, + "expected_with_subtrees": 4.47213595499958, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "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.472136, + "expected_with_subtrees": 4.242641, + + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0.0, + "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, + } + ], + "total_length_per_neurite": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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": [5., 10.828427], + "expected_with_subtrees": [5., 5.414214], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [5.414214], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [5.], + "expected_with_subtrees": [5.], + } + ], + "total_area_per_neurite" : [ + { + # total length x 2piR + "kwargs": {"neurite_type": NeuriteType.all}, + "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": [3.141593, 6.803702], + "expected_with_subtrees": [3.141593, 3.401851], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [3.401851], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "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.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.15708 , 0.340185], + "expected_with_subtrees": [0.15708 , 0.170093], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.170093], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.15708], + "expected_with_subtrees": [0.15708], + } + ], + "trunk_origin_azimuths": [ # Not applicable to distal subtrees + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [3.1415927, 0.0, 0.0], + "expected_with_subtrees": [3.1415927, 0.0, 0.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [3.1415927, 0.0], + "expected_with_subtrees": [3.1415927, 0.0], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "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 + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0.0, 1.5707964, -1.5707964], + "expected_with_subtrees": [0.0, 1.5707964, -1.5707964], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [0.0, 1.5707964], + "expected_with_subtrees": [0.0, 1.5707964], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [-1.570796], + "expected_with_subtrees": [-1.570796], + }, + ], + "trunk_vectors": [ + { + "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.]], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [[-1., 0., 0.], [0., 1., 0.]], + "expected_with_subtrees": [[-1., 0., 0.], [0., 1., 0.]], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [[1., 2., 0.]], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [[0., -1., 0.]], + "expected_with_subtrees": [[0., -1., 0.]], + }, + + ], + "trunk_angles": [ # Not applicable to distal subtrees + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1.570796, 3.141592, 1.570796], + "expected_with_subtrees": [1.570796, 3.141592, 1.570796], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1.5707964, 1.570796], + "expected_with_subtrees": [1.5707964, 1.570796], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.], + "expected_with_subtrees": [0.], + }, + ], + "trunk_angles_from_vector": [ + { + "kwargs": {"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.], + ], + }, + { + "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.]], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [[0.463648, -0.463648, 0.]], + }, + + ], + "trunk_angles_inter_types": [ + { + "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": [ + { + "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], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [0.1, 0.1], + "expected_with_subtrees": [0.1, 0.1], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.1], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.1], + "expected_with_subtrees": [0.1], + }, + ], + "trunk_section_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1., 1.414213, 1.], + "expected_with_subtrees": [1., 1.414213, 1.414213, 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1., 1.414213], + "expected_with_subtrees": [1., 1.414213], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.414213], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.], + "expected_with_subtrees": [1.], + }, + ], + "number_of_neurites": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 3, + "expected_with_subtrees": 4, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 2, + "expected_with_subtrees": 2, + }, + { + "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, + }, + ], + "neurite_volume_density": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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": [0.235619, 0.063785], + "expected_with_subtrees": [0.235619, 0.255139], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.170093], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.235619], + "expected_with_subtrees": [0.235619], + }, + ], + "sholl_crossings": [ + { + "kwargs": {"neurite_type": NeuriteType.all, "radii": [1.5, 3.5]}, + "expected_wout_subtrees": [3, 2], + "expected_with_subtrees": [3, 2], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite, "radii": [1.5, 3.5]}, + "expected_wout_subtrees": [2, 2], + "expected_with_subtrees": [2, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon, "radii": [1.5, 3.5]}, + "expected_wout_subtrees": [0, 0], + "expected_with_subtrees": [0, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite, "radii": [1.5, 3.5]}, + "expected_wout_subtrees": [1, 0], + "expected_with_subtrees": [1, 0], + }, + ], + "sholl_frequency": [ + { + "kwargs": {"neurite_type": NeuriteType.all, "step_size": 3}, + "expected_wout_subtrees": [0, 2], + "expected_with_subtrees": [0, 2], + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite, "step_size": 3}, + "expected_wout_subtrees": [0, 2], + "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], + "expected_with_subtrees": [0, 1], + }, + + ], + "total_width": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 6.0, + "expected_with_subtrees": 6.0, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 6.0, + "expected_with_subtrees": 4.0, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 2.0, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 1.0, + "expected_with_subtrees": 1.0, + }, + ], + "total_height": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 7.0, + "expected_with_subtrees": 7.0, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 4.0, + "expected_with_subtrees": 4.0, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 2.0, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 2.0, + "expected_with_subtrees": 2.0, + }, + ], + "total_depth": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 2.0, + "expected_with_subtrees": 2.0, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 2.0, + "expected_with_subtrees": 2.0, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 2.0, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 2.0, + "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, + }, + ], + "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, + }, + ], + "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()) + + assert not features_not_tested, ( + "The following morphology tests need to be included in the mixed morphology tests:\n" + f"{features_not_tested}" + ) + + return _dispatch_features(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): + _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 +): + _assert_feature_equal(mixed_morph, feature_name, expected, kwargs, use_subtrees=True) + + +def _neurite_features(mode): + + features = { + "number_of_segments": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 19, + "expected_with_subtrees": 19, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 14, + "expected_with_subtrees": 9, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0, + "expected_with_subtrees": 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 5, + "expected_with_subtrees": 5, + }, + ], + "number_of_leaves": [ + { + "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, + }, + ], + "total_length": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 20.828427, + "expected_with_subtrees": 20.828427, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 15.828427, + "expected_with_subtrees": 10.414214, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0.0, + "expected_with_subtrees": 5.414214, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 5., + "expected_with_subtrees": 5., + } + ], + "total_area": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 13.086887, + "expected_with_subtrees": 13.086887, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 9.945294, + "expected_with_subtrees": 6.543443, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0., + "expected_with_subtrees": 3.401851, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 3.141593, + "expected_with_subtrees": 3.141593, + } + ], + "total_volume": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 0.654344, + "expected_with_subtrees": 0.654344, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 0.497265, + "expected_with_subtrees": 0.327172, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0., + "expected_with_subtrees": 0.170093, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 0.15708, + "expected_with_subtrees": 0.15708, + } + ], + "section_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [1.] * 5 + [1.414214, 2., 1., 1.] + [1.414214, 1., 1., 1., 1.] + [1.] * 5, + "expected_with_subtrees": + [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.] * 5 + [1.414214, 2., 1., 1.] + [1.414214, 1., 1., 1., 1], + "expected_with_subtrees": + [1.] * 5 + [1.414214, 2., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.414214, 1., 1., 1., 1.], + + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.] * 5, + "expected_with_subtrees": [1.] * 5, + } + ], + "section_areas": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [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] * 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] * 5 + [0.888577, 1.256637, 0.628319, 0.628319] + + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319], + "expected_with_subtrees": + [0.628318] * 5 + [0.888577, 1.256637, 0.628319, 0.628319], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.888577, 0.628319, 0.628319, 0.628319, 0.628319], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.628318] * 5, + "expected_with_subtrees": [0.628318] * 5, + } + + ], + "section_volumes": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [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.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.031416] * 5 + [0.044429, 0.062832, 0.031416, 0.031416] + + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416], + "expected_with_subtrees": + [0.031416] * 5 + [0.044429, 0.062832, 0.031416, 0.031416], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [0.044429, 0.031416, 0.031416, 0.031416, 0.031416], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.031415] * 5, + "expected_with_subtrees": [0.031415] * 5, + } + ], + "section_tortuosity": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1.0] * 19, + "expected_with_subtrees": [1.0] * 19, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "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] * 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.0] * 5, + "expected_with_subtrees": [1.0] * 5, + } + ], + "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. , 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. , 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. , 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. , 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, 2.44949 , 2.44949], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1., 2., 2.236068, 2.236068, 1.414214], + "expected_with_subtrees": [1., 2., 2.236068, 2.236068, 1.414214], + } + + ], + "section_term_radial_distances": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [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.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.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.44949 , 2.44949], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [2.236068, 2.236068, 1.414214], + "expected_with_subtrees": [2.236068, 2.236068, 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., 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., 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.414214, 2.236068], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1., 2.], + "expected_with_subtrees": [1., 2.], + } + ], + "section_end_distances": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [1.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.] + + [1.] * 5, + "expected_with_subtrees": + [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.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.], + "expected_with_subtrees": + [1.] * 5 + + [1.414214, 2., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.414214, 1., 1., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.] * 5, + "expected_with_subtrees": [1.] * 5, + } + ], + "section_term_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [1.] * 11, + "expected_with_subtrees": [1.] * 11, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": [1.] * 8, + "expected_with_subtrees": [1.] * 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.] * 3, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.] * 3, + "expected_with_subtrees": [1.] * 3, + } + ], + "section_taper_rates": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0.0] * 19, + "expected_with_subtrees": [0.0] * 19, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "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] * 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "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., 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., 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, 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1., 1.], + "expected_with_subtrees": [1., 1.], + }, + ], + "section_branch_orders": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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, 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, 3, 3], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "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, 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, 1, 0, 1, 1, 2], + "expected_with_subtrees": [0, 1, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1, 2], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0, 1], + "expected_with_subtrees": [0, 1], + }, + ], + "section_term_branch_orders": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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": [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, 3, 3], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [2, 2, 1], + "expected_with_subtrees": [2, 2, 1], + }, + ], + "section_strahler_orders": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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, 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, 2, 1, 1], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "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.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.] + + [1.] * 5, + "expected_with_subtrees": + [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.] * 5 + + [1.414214, 2., 1., 1.] + + [1.414214, 1., 1., 1., 1.], + "expected_with_subtrees": + [1.] * 5 + + [1.414214, 2., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [1.414214, 1., 1., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.] * 5, + "expected_with_subtrees": [1.] * 5, + } + ], + "segment_areas": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [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.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.628319] * 5 + + [0.888577, 1.256637, 0.628319, 0.628319] + + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319], + "expected_with_subtrees": + [0.628319] * 5 + + [0.888577, 1.256637, 0.628319, 0.628319], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": + [0.888577, 0.628319, 0.628319, 0.628319, 0.628319], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.628318] * 5, + "expected_with_subtrees": [0.628318] * 5, + } + ], + "segment_volumes": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": + [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] * 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] * 5 + + [0.044429, 0.062832, 0.031416, 0.031416] + + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416], + "expected_with_subtrees": + [0.031415] * 5 + + [0.044429, 0.062832, 0.031416, 0.031416], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": + [0.044429, 0.031416, 0.031416, 0.031416, 0.031416], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.031415] * 5, + "expected_with_subtrees": [0.031415] * 5, + } + ], + "segment_radii": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": [0.1] * 19, + "expected_with_subtrees": [0.1] * 19, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "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] * 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "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] * 19, + "expected_with_subtrees": [0.0] * 19, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "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] * 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [0.0] * 5, + "expected_with_subtrees": [0.0] * 5, + }, + ], + "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, 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, 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, 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, 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, 2.291288, 2.291288], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "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], [-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], [-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], [-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], [-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], + [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.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": [ + { + "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": 19, + "expected_with_subtrees": 19, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 14, + "expected_with_subtrees": 9, + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": 0, + "expected_with_subtrees": 5, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": 5, + "expected_with_subtrees": 5, + }, + ], + "number_of_bifurcations": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 8, + "expected_with_subtrees": 7, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 6, + "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, + }, + ], + "number_of_forking_points": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "expected_wout_subtrees": 8, + "expected_with_subtrees": 7, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "expected_wout_subtrees": 6, + "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, + }, + ], + "local_bifurcation_angles": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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": + [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": [1.570796, 3.141593], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "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": + [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": + [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": [1.570796, 3.141593], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "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] * 8, + "expected_with_subtrees": [1.0] * 7, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "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] * 2, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [1.0] * 2, + "expected_with_subtrees": [1.0] * 2, + }, + ], + "partition_pairs": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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": + [[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, 3.0], [1.0, 1.0]], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "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] * 8, + "expected_with_subtrees": [2.0] * 7, + }, + { + "kwargs": {"neurite_type": NeuriteType.basal_dendrite}, + "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] * 2, + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [2.0] * 2, + "expected_with_subtrees": [2.0] * 2, + }, + ], + "bifurcation_partitions": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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": [3., 1., 1.666667, 1., 3., 1. ], + "expected_with_subtrees": [3., 1., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [3., 1.], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [3., 1.], + "expected_with_subtrees": [3., 1.], + }, + ], + "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, 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., 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., 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.414214, 2.414214, 2.414214, 3.414214, 3.414214], + }, + ], + "terminal_path_lengths": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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": + [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.414214, 3.414214, 3.414214], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [3., 3., 2.], + "expected_with_subtrees": [3., 3., 2.], + }, + ], + "principal_direction_extents": [ + { + "kwargs": {"neurite_type": NeuriteType.all}, + "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": [2., 3.596771], + "expected_with_subtrees": [2., 3.154926], + }, + { + "kwargs": {"neurite_type": NeuriteType.axon}, + "expected_wout_subtrees": [], + "expected_with_subtrees": [2.235207], + }, + { + "kwargs": {"neurite_type": NeuriteType.apical_dendrite}, + "expected_wout_subtrees": [2.], + "expected_with_subtrees": [2.], + }, + ], + "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], + }, + { + "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], + }, + ], + "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" + ) + + 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") +) +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) +