From ec04df68871b04f178e26aff404db1089c43bbec Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 5 Apr 2022 00:21:24 +0200 Subject: [PATCH 1/2] Refactor get --- neurom/features/__init__.py | 207 ++++++++++++++++++---------------- neurom/features/morphology.py | 4 +- neurom/features/neurite.py | 2 +- neurom/features/population.py | 2 +- 4 files changed, 111 insertions(+), 104 deletions(-) diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 924de8d1..7e9aad7e 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -37,12 +37,14 @@ >>> ax_sec_len = features.get('section_lengths', m, neurite_type=neurom.AXON) """ import operator +import collections.abc from enum import Enum -from functools import reduce +from functools import reduce, wraps from neurom.core import Population, Morphology, Neurite from neurom.core.morphology import iter_neurites from neurom.core.types import NeuriteType, tree_type_checker as is_type +from neurom.utils import flatten from neurom.exceptions import NeuroMError _NEURITE_FEATURES = {} @@ -54,84 +56,10 @@ class NameSpace(Enum): """The level of morphology abstraction that feature applies to.""" NEURITE = 'neurite' NEURON = 'morphology' + MORPHOLOGY = 'morphology' POPULATION = 'population' -def _flatten_feature(feature_shape, feature_value): - """Flattens feature values. Applies for population features for backward compatibility.""" - if feature_shape == (): - return feature_value - return reduce(operator.concat, feature_value, []) - - -def _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs): - """Collects neurite feature values appropriately to feature's shape.""" - kwargs.pop('neurite_type', None) # there is no 'neurite_type' arg in _NEURITE_FEATURES - return reduce(operator.add, - (feature_(n, **kwargs) for n in iter_neurites(obj, filt=neurite_filter)), - 0 if feature_.shape == () else []) - - -def _get_feature_value_and_func(feature_name, obj, **kwargs): - """Obtain a feature from a set of morphology objects. - - Arguments: - feature_name(string): feature to extract - obj (Neurite|Morphology|Population): neurite, morphology or population - kwargs: parameters to forward to underlying worker functions - - Returns: - Tuple(List|Number, function): A tuple (feature, func) of the feature value and its function. - Feature value can be a list or a number. - """ - # pylint: disable=too-many-branches - is_obj_list = isinstance(obj, (list, tuple)) - if not isinstance(obj, (Neurite, Morphology, Population)) and not is_obj_list: - raise NeuroMError('Only Neurite, Morphology, Population or list, tuple of Neurite,' - ' Morphology can be used for feature calculation') - - neurite_filter = is_type(kwargs.get('neurite_type', NeuriteType.all)) - res, feature_ = None, None - - if isinstance(obj, Neurite) or (is_obj_list and isinstance(obj[0], Neurite)): - # input is a neurite or a list of neurites - if feature_name in _NEURITE_FEATURES: - assert 'neurite_type' not in kwargs, 'Cant apply "neurite_type" arg to a neurite with' \ - ' a neurite feature' - feature_ = _NEURITE_FEATURES[feature_name] - if isinstance(obj, Neurite): - res = feature_(obj, **kwargs) - else: - res = [feature_(s, **kwargs) for s in obj] - elif isinstance(obj, Morphology): - # input is a morphology - if feature_name in _MORPHOLOGY_FEATURES: - feature_ = _MORPHOLOGY_FEATURES[feature_name] - res = feature_(obj, **kwargs) - elif feature_name in _NEURITE_FEATURES: - feature_ = _NEURITE_FEATURES[feature_name] - res = _get_neurites_feature_value(feature_, obj, neurite_filter, kwargs) - elif isinstance(obj, Population) or (is_obj_list and isinstance(obj[0], Morphology)): - # input is a morphology population or a list of morphs - if feature_name in _POPULATION_FEATURES: - feature_ = _POPULATION_FEATURES[feature_name] - res = feature_(obj, **kwargs) - elif feature_name in _MORPHOLOGY_FEATURES: - feature_ = _MORPHOLOGY_FEATURES[feature_name] - res = _flatten_feature(feature_.shape, [feature_(n, **kwargs) for n in obj]) - elif feature_name in _NEURITE_FEATURES: - feature_ = _NEURITE_FEATURES[feature_name] - res = _flatten_feature( - feature_.shape, - [_get_neurites_feature_value(feature_, n, neurite_filter, kwargs) for n in obj]) - - if res is None or feature_ is None: - raise NeuroMError(f'Cant apply "{feature_name}" feature. Please check that it exists, ' - 'and can be applied to your input. See the features documentation page.') - - return res, feature_ - - def get(feature_name, obj, **kwargs): """Obtain a feature from a set of morphology objects. @@ -147,31 +75,29 @@ 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] + if isinstance(obj, Neurite): + return _NEURITE_FEATURES[feature_name](obj, **kwargs) + if isinstance(obj, Morphology): + return _MORPHOLOGY_FEATURES[feature_name](obj, **kwargs) -def _register_feature(namespace: NameSpace, name, func, shape): - """Register a feature to be applied. + if isinstance(obj, Population): + return _POPULATION_FEATURES[feature_name](obj, **kwargs) - Upon registration, an attribute 'shape' containing the expected - shape of the function return is added to 'func'. + if isinstance(obj, collections.abc.Sequence): - Arguments: - namespace(string): a namespace, see :class:`NameSpace` - name(string): name of the feature, used to access the feature via `neurom.features.get()`. - func(callable): single parameter function of a neurite. - shape(tuple): the expected shape of the feature values - """ - setattr(func, 'shape', shape) - _map = {NameSpace.NEURITE: _NEURITE_FEATURES, - NameSpace.NEURON: _MORPHOLOGY_FEATURES, - NameSpace.POPULATION: _POPULATION_FEATURES} - if name in _map[namespace]: - raise NeuroMError(f'A feature is already registered under "{name}"') - _map[namespace][name] = func + if isinstance(obj[0], Neurite): + return [_NEURITE_FEATURES[feature_name](neurite, **kwargs) for neurite in obj] + + if isinstance(obj[0], Morphology): + return _POPULATION_FEATURES[feature_name](obj, **kwargs) + + raise NeuroMError(f'Cant apply "{feature_name}" feature. Please check that it exists, ' + 'and can be applied to your input. See the features documentation page.' + ) -def feature(shape, namespace: NameSpace, name=None): +def feature(shape, namespace: NameSpace, name=None, is_reducible=True): """Feature decorator to automatically register the feature in the appropriate namespace. Arguments: @@ -180,16 +106,97 @@ def feature(shape, namespace: NameSpace, name=None): name(string): name of the feature, used to access the feature via `neurom.features.get()`. """ - def inner(func): - _register_feature(namespace, name or func.__name__, func, shape) - return func + def inner(feature_function): + _register_feature( + namespace=namespace, + name=name or feature_function.__name__, + func=feature_function, + shape=shape, + is_reducible=is_reducible, + ) + return feature_function return inner +def _shape_dependent_flatten(obj, shape): + return obj if shape == () else reduce(operator.concat, obj, []) + + +def _register_feature(namespace, name, func, shape, is_reducible=True): + + def apply_neurite_feature_to_population(func): + def apply_to_population(population, **kwargs): + return _shape_dependent_flatten( + [_get_neurites_feature_value(func, shape, morph, kwargs) for morph in population], + shape, + ) + return apply_to_population + + def apply_neurite_feature_to_morphology(func): + def apply_to_morphology(morphology, **kwargs): + return _get_neurites_feature_value(func, shape, morphology, kwargs) + return apply_to_morphology + + def apply_morphology_feature_to_population(func): + def apply_to_population(population, **kwargs): + return _shape_dependent_flatten( + [func(morphology, **kwargs) for morphology in population], + shape, + ) + return apply_to_population + + levels = (NameSpace.NEURITE, NameSpace.MORPHOLOGY, NameSpace.POPULATION) + + levels_map = { + NameSpace.NEURITE: _NEURITE_FEATURES, + NameSpace.NEURON: _MORPHOLOGY_FEATURES, + NameSpace.POPULATION: _POPULATION_FEATURES + } + + if name in levels_map[namespace]: + raise NeuroMError(f'A feature is already registered under "{name}"') + + levels_map[namespace][name] = func + upstream_levels = levels[levels.index(namespace) + 1:] + + if is_reducible: + + levels_reduce = { + (NameSpace.POPULATION, NameSpace.MORPHOLOGY): apply_morphology_feature_to_population, + (NameSpace.POPULATION, NameSpace.NEURITE): apply_neurite_feature_to_population, + (NameSpace.MORPHOLOGY, NameSpace.NEURITE): apply_neurite_feature_to_morphology, + } + + for level in upstream_levels: + if name not in levels_map[level]: + levels_map[level][name] = levels_reduce[(level, namespace)](func) + + +from copy import deepcopy + +def _get_neurites_feature_value(feature_, shape, obj, kwargs): + """Collects neurite feature values appropriately to feature's shape.""" + + kwargs = deepcopy(kwargs) + + if "neurite_type" in kwargs: + neurite_type = kwargs["neurite_type"] + del kwargs["neurite_type"] + else: + neurite_type = NeuriteType.all + + return reduce( + operator.add, + (feature_(n, **kwargs) for n in iter_neurites(obj, filt=is_type(neurite_type))), + 0 if shape == () else [] + ) + + + # These imports are necessary in order to register the features -from neurom.features import neurite, morphology, \ - population # noqa, pylint: disable=wrong-import-position +# noqa, pylint: disable=wrong-import-position +from neurom.features import neurite, morphology, population def _features_catalogue(): diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index bc7e096a..eb9898f1 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -504,7 +504,7 @@ def _count_crossings(neurite, radius): for r in radii] -@feature(shape=(...,)) +@feature(shape=(...,), is_reducible=False) def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None): """Perform Sholl frequency calculations on a morph. @@ -580,7 +580,7 @@ def total_depth(morph, neurite_type=NeuriteType.all): @feature(shape=()) -def volume_density(morph, neurite_type=NeuriteType.all): +def volume_density(morph, neurite_type=NeuriteType.all, is_redusible=False): """Get the volume density. The volume density is defined as the ratio of the neurite volume and diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index 6aac5e9f..a17f19fc 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -404,7 +404,7 @@ def terminal_path_lengths(neurite): return _map_sections(sf.section_path_length, neurite, Section.ileaf) -@feature(shape=()) +@feature(shape=(), is_reducible=False) def volume_density(neurite): """Get the volume density. diff --git a/neurom/features/population.py b/neurom/features/population.py index 98cbd4de..c5cd75ae 100644 --- a/neurom/features/population.py +++ b/neurom/features/population.py @@ -52,7 +52,7 @@ feature = partial(feature, namespace=NameSpace.POPULATION) -@feature(shape=(...,)) +@feature(shape=(...,), is_reducible=False) def sholl_frequency(morphs, neurite_type=NeuriteType.all, step_size=10, bins=None): """Perform Sholl frequency calculations on a population of morphs. From a8de3deeb6546a593d032e3e8df97e65ed612b0d Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 5 Apr 2022 14:51:49 +0200 Subject: [PATCH 2/2] Refactoring --- neurom/apps/morph_stats.py | 7 +- neurom/features/__init__.py | 254 +++++++++++++++++++--------- neurom/features/morphology.py | 4 +- neurom/features/neurite.py | 2 +- neurom/features/population.py | 2 +- tests/features/test_get_features.py | 5 +- 6 files changed, 188 insertions(+), 86 deletions(-) diff --git a/neurom/apps/morph_stats.py b/neurom/apps/morph_stats.py index 9cdd7de0..eee780ca 100644 --- a/neurom/apps/morph_stats.py +++ b/neurom/apps/morph_stats.py @@ -49,8 +49,9 @@ from neurom.apps import get_config from neurom.core.morphology import Morphology from neurom.exceptions import ConfigError -from neurom.features import _NEURITE_FEATURES, _MORPHOLOGY_FEATURES, _POPULATION_FEATURES, \ - _get_feature_value_and_func +from neurom.features import ( + _NEURITE_FEATURES, _MORPHOLOGY_FEATURES, _POPULATION_FEATURES, get_feature_value_and_func +) from neurom.io.utils import get_files_by_path from neurom.utils import flatten, NeuromJSON, warn_deprecated @@ -121,7 +122,7 @@ def _get_feature_stats(feature_name, morphs, modes, kwargs): If the feature is 2-dimensional, the feature is flattened on its last axis """ data = {} - value, func = _get_feature_value_and_func(feature_name, morphs, **kwargs) + value, func = get_feature_value_and_func(feature_name, morphs, **kwargs) shape = func.shape if len(shape) > 2: raise ValueError(f'Len of "{feature_name}" feature shape must be <= 2') # pragma: no cover diff --git a/neurom/features/__init__.py b/neurom/features/__init__.py index 7e9aad7e..f5b56f45 100644 --- a/neurom/features/__init__.py +++ b/neurom/features/__init__.py @@ -37,6 +37,8 @@ >>> ax_sec_len = features.get('section_lengths', m, neurite_type=neurom.AXON) """ import operator + +from copy import deepcopy import collections.abc from enum import Enum from functools import reduce, wraps @@ -60,6 +62,14 @@ class NameSpace(Enum): POPULATION = 'population' +_FEATURE_CATEGORIES = { + NameSpace.NEURITE: _NEURITE_FEATURES, + NameSpace.NEURON: _MORPHOLOGY_FEATURES, + NameSpace.MORPHOLOGY: _MORPHOLOGY_FEATURES, + NameSpace.POPULATION: _POPULATION_FEATURES, +} + + def get(feature_name, obj, **kwargs): """Obtain a feature from a set of morphology objects. @@ -75,29 +85,64 @@ def get(feature_name, obj, **kwargs): Returns: List|Number: feature value as a list or a single number. """ - if isinstance(obj, Neurite): - return _NEURITE_FEATURES[feature_name](obj, **kwargs) + return get_feature_value_and_func(feature_name, obj, **kwargs)[0] + + +def get_feature_value_and_func(feature_name, obj, **kwargs): + """Obtain a feature's values and corresponding function from a set of morphology objects. + + Features can be either Neurite, Morphology or Population features. For Neurite features see + :mod:`neurom.features.neurite`. For Morphology features see :mod:`neurom.features.morphology`. + For Population features see :mod:`neurom.features.population`. + + Arguments: + feature_name(string): feature to extract + obj: a morphology, a morphology population or a neurite tree + kwargs: parameters to forward to underlying worker functions + + Returns: + List|Number: feature value as a list or a single number. + Callable: feature function used to calculate the value. + """ + try: + + if isinstance(obj, Neurite): + feature_function = _NEURITE_FEATURES[feature_name] + return feature_function(obj, **kwargs), feature_function - if isinstance(obj, Morphology): - return _MORPHOLOGY_FEATURES[feature_name](obj, **kwargs) + if isinstance(obj, Morphology): + feature_function = _MORPHOLOGY_FEATURES[feature_name] + return feature_function(obj, **kwargs), feature_function - if isinstance(obj, Population): - return _POPULATION_FEATURES[feature_name](obj, **kwargs) + if isinstance(obj, Population): + feature_function = _POPULATION_FEATURES[feature_name] + return feature_function(obj, **kwargs), feature_function - if isinstance(obj, collections.abc.Sequence): + if isinstance(obj, collections.abc.Sequence): - if isinstance(obj[0], Neurite): - return [_NEURITE_FEATURES[feature_name](neurite, **kwargs) for neurite in obj] + if isinstance(obj[0], Neurite): + feature_function = _NEURITE_FEATURES[feature_name] + return [feature_function(neu, **kwargs) for neu in obj], feature_function - if isinstance(obj[0], Morphology): - return _POPULATION_FEATURES[feature_name](obj, **kwargs) + if isinstance(obj[0], Morphology): + feature_function = _POPULATION_FEATURES[feature_name] + return feature_function(obj, **kwargs), feature_function - raise NeuroMError(f'Cant apply "{feature_name}" feature. Please check that it exists, ' - 'and can be applied to your input. See the features documentation page.' + except Exception as e: + + raise NeuroMError( + f"Cant apply '{feature_name}' feature on {type(obj)}. Please check that it exists, " + "and can be applied to your input. See the features documentation page." + ) from e + + raise NeuroMError( + "Only Neurite, Morphology, Population or list, tuple of Neurite, Morphology can be used for" + " feature calculation." + f"Got {type(obj)} instead. See the features documentation page." ) -def feature(shape, namespace: NameSpace, name=None, is_reducible=True): +def feature(shape, namespace: NameSpace, name=None): """Feature decorator to automatically register the feature in the appropriate namespace. Arguments: @@ -107,96 +152,153 @@ def feature(shape, namespace: NameSpace, name=None, is_reducible=True): """ def inner(feature_function): - _register_feature( - namespace=namespace, - name=name or feature_function.__name__, - func=feature_function, - shape=shape, - is_reducible=is_reducible, - ) + _register_feature(namespace, name or feature_function.__name__, feature_function, shape) return feature_function return inner -def _shape_dependent_flatten(obj, shape): - return obj if shape == () else reduce(operator.concat, obj, []) - - -def _register_feature(namespace, name, func, shape, is_reducible=True): - - def apply_neurite_feature_to_population(func): - def apply_to_population(population, **kwargs): - return _shape_dependent_flatten( - [_get_neurites_feature_value(func, shape, morph, kwargs) for morph in population], - shape, - ) - return apply_to_population +def _register_feature(namespace: NameSpace, name, func, shape): + """Register a feature to be applied. - def apply_neurite_feature_to_morphology(func): - def apply_to_morphology(morphology, **kwargs): - return _get_neurites_feature_value(func, shape, morphology, kwargs) - return apply_to_morphology + Upon registration, an attribute 'shape' containing the expected + shape of the function return is added to 'func'. - def apply_morphology_feature_to_population(func): - def apply_to_population(population, **kwargs): - return _shape_dependent_flatten( - [func(morphology, **kwargs) for morphology in population], - shape, - ) - return apply_to_population - - levels = (NameSpace.NEURITE, NameSpace.MORPHOLOGY, NameSpace.POPULATION) - - levels_map = { - NameSpace.NEURITE: _NEURITE_FEATURES, - NameSpace.NEURON: _MORPHOLOGY_FEATURES, - NameSpace.POPULATION: _POPULATION_FEATURES - } - - if name in levels_map[namespace]: + Arguments: + namespace(string): a namespace, see :class:`NameSpace` + name(string): name of the feature, used to access the feature via `neurom.features.get()`. + func(callable): single parameter function of a neurite. + shape(tuple): the expected shape of the feature values + """ + if name in _FEATURE_CATEGORIES[namespace]: raise NeuroMError(f'A feature is already registered under "{name}"') - levels_map[namespace][name] = func - upstream_levels = levels[levels.index(namespace) + 1:] + setattr(func, "shape", shape) - if is_reducible: + _FEATURE_CATEGORIES[namespace][name] = func - levels_reduce = { - (NameSpace.POPULATION, NameSpace.MORPHOLOGY): apply_morphology_feature_to_population, - (NameSpace.POPULATION, NameSpace.NEURITE): apply_neurite_feature_to_population, - (NameSpace.MORPHOLOGY, NameSpace.NEURITE): apply_neurite_feature_to_morphology, - } - for level in upstream_levels: - if name not in levels_map[level]: - levels_map[level][name] = levels_reduce[(level, namespace)](func) +def _flatten_feature(feature_value, feature_shape): + """Flattens feature values. Applies for population features for backward compatibility.""" + return feature_value if feature_shape == () else reduce(operator.concat, feature_value, []) -from copy import deepcopy - -def _get_neurites_feature_value(feature_, shape, obj, kwargs): +def _get_neurites_feature_value(feature_, obj, kwargs): """Collects neurite feature values appropriately to feature's shape.""" - kwargs = deepcopy(kwargs) + # there is no 'neurite_type' arg in _NEURITE_FEATURES if "neurite_type" in kwargs: neurite_type = kwargs["neurite_type"] del kwargs["neurite_type"] else: neurite_type = NeuriteType.all - return reduce( - operator.add, - (feature_(n, **kwargs) for n in iter_neurites(obj, filt=is_type(neurite_type))), - 0 if shape == () else [] + per_neurite_values = ( + feature_(n, **kwargs) for n in iter_neurites(obj, filt=is_type(neurite_type)) ) + return reduce(operator.add, per_neurite_values, 0 if feature_.shape == () else []) + + +def _transform_downstream_features_to_upstream_feature_categories(features): + """Adds each feature to all upstream feature categories, adapted for the respective objects. + + If a feature is already defined in the module of an upstream category, it is not overwritten. + This allows to achieve both reducible features, which can be defined for instance at the neurite + category and then automatically added to the morphology and population categories transformed to + work with morphology and population objects respetively. + + However, if a feature is not reducible, which means that an upstream category is not comprised + by the accumulation/sum of its components, the feature should be defined on each category + module so that the module logic is used instead. + + After the end of this function the _NEURITE_FEATURES, _MORPHOLOGY_FEATURES, _POPULATION_FEATURES + are updated so that all features in neurite features are also available in morphology and + population dictionaries, and all morphology features are available in the population dictionary. + + Args: + features: Dictionary with feature categories. + + Notes: + Category Upstream Categories + -------- ----------------- + morphology population + neurite morphology, population + """ + def apply_neurite_feature_to_population(func): + """Transforms a feature in _NEURITE_FEATURES so that it can be applied to a population. + + Args: + func: Feature function. + + Returns: + Transformed neurite function to be applied on a population of morphologies. + """ + def apply_to_population(pop, **kwargs): + + per_morphology_values = [ + _get_neurites_feature_value(func, morph, kwargs) for morph in pop + ] + return _flatten_feature(per_morphology_values, func.shape) + + return apply_to_population + + def apply_neurite_feature_to_morphology(func): + """Transforms a feature in _NEURITE_FEATURES so that it can be applied on neurites. + + Args: + func: Feature function. + + Returns: + Transformed neurite function to be applied on a morphology. + """ + def apply_to_morphology(morph, **kwargs): + return _get_neurites_feature_value(func, morph, kwargs) + return apply_to_morphology + + def apply_morphology_feature_to_population(func): + """Transforms a feature in _MORPHOLOGY_FEATURES so that it can be applied to a population. + + Args: + func: Feature function. + + Returns: + Transformed morphology function to be applied on a population of morphologies. + """ + def apply_to_population(pop, **kwargs): + per_morphology_values = [func(morph, **kwargs) for morph in pop] + return _flatten_feature(per_morphology_values, func.shape) + return apply_to_population + + transformations = { + (NameSpace.POPULATION, NameSpace.MORPHOLOGY): apply_morphology_feature_to_population, + (NameSpace.POPULATION, NameSpace.NEURITE): apply_neurite_feature_to_population, + (NameSpace.MORPHOLOGY, NameSpace.NEURITE): apply_neurite_feature_to_morphology, + } + + for (upstream_category, category), transformation in transformations.items(): + + features = _FEATURE_CATEGORIES[category] + upstream_features = _FEATURE_CATEGORIES[upstream_category] + + for feature_name, feature_function in features.items(): + + if feature_name in upstream_features: + continue + + upstream_features[feature_name] = transformation(feature_function) + setattr(upstream_features[feature_name], "shape", feature_function.shape) # These imports are necessary in order to register the features -# noqa, pylint: disable=wrong-import-position -from neurom.features import neurite, morphology, population +from neurom.features import neurite, morphology, population # noqa, pylint: disable=wrong-import-position + + +# Update the feature dictionaries so that features from lower categories are transformed and usable +# by upstream categories. For example, a neurite feature will be added to morphology and population +# feature dictionaries, transformed so that it works with the respective objects. +_transform_downstream_features_to_upstream_feature_categories(_FEATURE_CATEGORIES) def _features_catalogue(): diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index eb9898f1..bc7e096a 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -504,7 +504,7 @@ def _count_crossings(neurite, radius): for r in radii] -@feature(shape=(...,), is_reducible=False) +@feature(shape=(...,)) def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None): """Perform Sholl frequency calculations on a morph. @@ -580,7 +580,7 @@ def total_depth(morph, neurite_type=NeuriteType.all): @feature(shape=()) -def volume_density(morph, neurite_type=NeuriteType.all, is_redusible=False): +def volume_density(morph, neurite_type=NeuriteType.all): """Get the volume density. The volume density is defined as the ratio of the neurite volume and diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py index a17f19fc..6aac5e9f 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -404,7 +404,7 @@ def terminal_path_lengths(neurite): return _map_sections(sf.section_path_length, neurite, Section.ileaf) -@feature(shape=(), is_reducible=False) +@feature(shape=()) def volume_density(neurite): """Get the volume density. diff --git a/neurom/features/population.py b/neurom/features/population.py index c5cd75ae..98cbd4de 100644 --- a/neurom/features/population.py +++ b/neurom/features/population.py @@ -52,7 +52,7 @@ feature = partial(feature, namespace=NameSpace.POPULATION) -@feature(shape=(...,), is_reducible=False) +@feature(shape=(...,)) def sholl_frequency(morphs, neurite_type=NeuriteType.all, step_size=10, bins=None): """Perform Sholl frequency calculations on a population of morphs. diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py index ae731323..675c4f9b 100644 --- a/tests/features/test_get_features.py +++ b/tests/features/test_get_features.py @@ -65,10 +65,9 @@ def _stats(seq): def test_get_raises(): - with pytest.raises(NeuroMError, - match='Only Neurite, Morphology, Population or list, tuple of Neurite, Morphology'): + with pytest.raises(NeuroMError, match="Only Neurite, Morphology, Population or list, tuple of Neurite, Morphology"): features.get('soma_radius', (n for n in POP)) - with pytest.raises(NeuroMError, match='Cant apply "invalid" feature'): + with pytest.raises(NeuroMError, match="Cant apply 'invalid' feature"): features.get('invalid', NRN)