Skip to content
This repository has been archived by the owner on Feb 26, 2025. It is now read-only.

Commit

Permalink
Use decorator to register feature (#860)
Browse files Browse the repository at this point in the history
# New feature:

The features are no longer registered as a hard coded list of functions but as follows:

```python
@feature(shape=[Shape.OnePerSegment], namespace='NEURITEFEATURES')
def segment_meander_angles(neurites, neurite_type=NeuriteType.all):
    """Inter-segment opening angles in a section."""
    ...
```

The 'shape' argument of the decorator holds the shape (a la numpy) of the
output result of the function.

# Deprecation

`register_neurite_feature` is now deprecated and is replaced by the `@feature` decorator.

# Fix

Fix issue with the documentation that was still using the old module name `neurom.fst` instead of `neurom.features`

# Note:
This commit will be used in a following PR to fix:
#859
  • Loading branch information
Benoit Coste authored Jan 7, 2021
1 parent f9561c8 commit ed3d900
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 126 deletions.
2 changes: 1 addition & 1 deletion apps/morph_stats
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ if __name__ == '__main__':
logging.DEBUG)[min(_args.verbose, 2)])

if _args.list:
print(nm.fst._get_doc()) # pylint: disable=W0212
print(nm.features._get_doc()) # pylint: disable=W0212
sys.exit(0)
elif not _args.datapath:
get_parser().print_usage()
Expand Down
6 changes: 4 additions & 2 deletions doc/source/api-dev.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ for developers of NeuroM itself.
:toctree: _neurom_build

neurom.morphmath
neurom.fst
neurom.fst.sectionfunc
neurom.features
neurom.features.neuritefunc
neurom.features.sectionfunc
neurom.features.bifurcationfunc
neurom.check.morphtree
neurom.check.structural_checks
neurom.check.neuron_checks
Expand Down
6 changes: 5 additions & 1 deletion neurom/apps/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from pathlib import Path
from subprocess import check_output

from click.testing import CliRunner
from mock import MagicMock, patch
from nose.tools import assert_equal
from nose.tools import assert_equal, ok_

from neurom.apps.cli import cli

Expand Down Expand Up @@ -40,3 +41,6 @@ def test_viewer_plotly(mock):
'--plane', 'xy'])
assert_equal(result.exit_code, 0)
mock.assert_called_once()

def test_morph_stat():
check_output(['morph_stats', '-l'])
137 changes: 62 additions & 75 deletions neurom/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,90 +35,30 @@
>>> ax_sec_len = features.get('section_lengths', nrn, neurite_type=neurom.AXON)
"""

from functools import partial, update_wrapper
import numpy as _np
import numpy as np

from neurom.features import neuritefunc as _nrt
from neurom.features import neuronfunc as _nrn
from neurom.core import NeuriteType as _ntype
from neurom.core import iter_neurites as _ineurites
from neurom.core.types import tree_type_checker as _is_type
from neurom.exceptions import NeuroMError
from neurom.utils import deprecated

_partition_asymmetry_length = partial(_nrt.partition_asymmetries, variant='length')
update_wrapper(_partition_asymmetry_length, _nrt.partition_asymmetries)

NEURITEFEATURES = {
'total_length': _nrt.total_length,
'total_length_per_neurite': _nrt.total_length_per_neurite,
'neurite_lengths': _nrt.total_length_per_neurite,
'terminal_path_lengths_per_neurite': _nrt.terminal_path_lengths_per_neurite,
'section_lengths': _nrt.section_lengths,
'section_term_lengths': _nrt.section_term_lengths,
'section_bif_lengths': _nrt.section_bif_lengths,
'neurite_volumes': _nrt.total_volume_per_neurite,
'neurite_volume_density': _nrt.neurite_volume_density,
'section_volumes': _nrt.section_volumes,
'section_areas': _nrt.section_areas,
'section_tortuosity': _nrt.section_tortuosity,
'section_path_distances': _nrt.section_path_lengths,
'number_of_sections': _nrt.number_of_sections,
'number_of_sections_per_neurite': _nrt.number_of_sections_per_neurite,
'number_of_neurites': _nrt.number_of_neurites,
'number_of_bifurcations': _nrt.number_of_bifurcations,
'number_of_forking_points': _nrt.number_of_forking_points,
'number_of_terminations': _nrt.number_of_terminations,
'section_branch_orders': _nrt.section_branch_orders,
'section_term_branch_orders': _nrt.section_term_branch_orders,
'section_bif_branch_orders': _nrt.section_bif_branch_orders,
'section_radial_distances': _nrt.section_radial_distances,
'section_bif_radial_distances': _nrt.section_bif_radial_distances,
'section_term_radial_distances': _nrt.section_term_radial_distances,
'section_end_distances': _nrt.section_end_distances,
'section_strahler_orders': _nrt.section_strahler_orders,
'local_bifurcation_angles': _nrt.local_bifurcation_angles,
'remote_bifurcation_angles': _nrt.remote_bifurcation_angles,
'partition': _nrt.bifurcation_partitions,
'partition_asymmetry': _nrt.partition_asymmetries,
'partition_pairs': _nrt.partition_pairs,
'partition_asymmetry_length': _partition_asymmetry_length,
'sibling_ratio': _nrt.sibling_ratios,
'diameter_power_relation': _nrt.diameter_power_relations,
'number_of_segments': _nrt.number_of_segments,
'segment_lengths': _nrt.segment_lengths,
'segment_areas': _nrt.segment_areas,
'segment_volumes': _nrt.segment_volumes,
'segment_radii': _nrt.segment_radii,
'segment_midpoints': _nrt.segment_midpoints,
'segment_taper_rates': _nrt.segment_taper_rates,
'segment_path_lengths': _nrt.segment_path_lengths,
'section_taper_rates': _nrt.section_taper_rates,
'segment_radial_distances': _nrt.segment_radial_distances,
'segment_meander_angles': _nrt.segment_meander_angles,
'principal_direction_extents': _nrt.principal_direction_extents,
'total_area_per_neurite': _nrt.total_area_per_neurite,
}

NEURONFEATURES = {
'soma_radii': _nrn.soma_radii,
'soma_surface_areas': _nrn.soma_surface_areas,
'soma_volumes': _nrn.soma_volumes,
'trunk_origin_radii': _nrn.trunk_origin_radii,
'trunk_origin_azimuths': _nrn.trunk_origin_azimuths,
'trunk_origin_elevations': _nrn.trunk_origin_elevations,
'trunk_section_lengths': _nrn.trunk_section_lengths,
'trunk_angles': _nrn.trunk_angles,
'trunk_vectors': _nrn.trunk_vectors,
'sholl_frequency': _nrn.sholl_frequency,
}
NEURITEFEATURES = dict()
NEURONFEATURES = dict()


@deprecated(
'`register_neurite_feature`',
'Please use the decorator `neurom.features.register.feature` to register custom features')
def register_neurite_feature(name, func):
"""Register a feature to be applied to neurites.
.. warning:: This feature has been deprecated in 1.6.0
Arguments:
name: name of the feature, used for access via get() function.
func: single parameter function of a neurite.
"""
if name in NEURITEFEATURES:
raise NeuroMError('Attempt to hide registered feature %s' % name)
Expand All @@ -127,10 +67,10 @@ def _fun(neurites, neurite_type=_ntype.all):
"""Wrap neurite function from outer scope and map into list."""
return list(func(n) for n in _ineurites(neurites, filt=_is_type(neurite_type)))

NEURONFEATURES[name] = _fun
_register_feature('NEURITEFEATURES', name, _fun, shape=(...,))


def get(feature, obj, **kwargs):
def get(feature_name, obj, **kwargs):
"""Obtain a feature from a set of morphology objects.
Arguments:
Expand All @@ -141,10 +81,14 @@ def get(feature, obj, **kwargs):
Returns:
features as a 1D or 2D numpy array.
"""
feature = (NEURITEFEATURES[feature] if feature in NEURITEFEATURES
else NEURONFEATURES[feature])
for feature_dict in (NEURITEFEATURES, NEURONFEATURES):
if feature_name in feature_dict:
feat = feature_dict[feature_name]
break
else:
raise NeuroMError(f'Unable to find feature: {feature_name}')

return _np.array(list(feature(obj, **kwargs)))
return np.array(list(feat(obj, **kwargs)))


_INDENT = ' ' * 4
Expand Down Expand Up @@ -178,3 +122,46 @@ def get_docstring(func):


get.__doc__ += _indent('\nFeatures:\n', 1) + _indent(_get_doc(), 2) # pylint: disable=no-member


def _register_feature(namespace, name, func, shape):
"""Register a feature to be applied.
Upon registration, an attribute 'shape' containing the expected
shape of the function return is added to 'func'.
Arguments:
namespace(string): a namespace (must be 'NEURITEFEATURES' or 'NEURONFEATURES')
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)

assert namespace in {'NEURITEFEATURES', 'NEURONFEATURES'}
feature_dict = globals()[namespace]

if name in feature_dict:
raise NeuroMError('Attempt to hide registered feature %s' % name)
feature_dict[name] = func


def feature(shape, namespace=None, name=None):
"""Feature decorator to automatically register the feature in the appropriate namespace.
Arguments:
shape(tuple): the expected shape of the feature values
namespace(string): a namespace (must be 'NEURITEFEATURES' or 'NEURONFEATURES')
name(string): name of the feature, used to access the feature via `neurom.features.get()`.
"""
def inner(func):
# Keep the old behavior that do not register those features
# TODO: this will be changed in the next commit
if not func.__name__.startswith('n_'):
_register_feature(namespace, name or func.__name__, func, shape)
return func
return inner


# These imports are necessary in order to register the features
from neurom.features import neuritefunc, neuronfunc # noqa, pylint: disable=wrong-import-position
Loading

0 comments on commit ed3d900

Please sign in to comment.