Skip to content

Commit

Permalink
Remove the cli command neurom features (#933)
Browse files Browse the repository at this point in the history
Instead a proper documentation is provided on that topic
  • Loading branch information
asanin-epfl authored Jun 14, 2021
1 parent c2617e4 commit 8d2fc96
Showing 10 changed files with 80 additions and 101 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -7,6 +7,9 @@ Version 2.3.0
:func:`neurom.features.neuritefunc.partition_asymmetries`.
- Follow the same morphology validation rules as in MorphIO. See the :ref:`doc page<validation>`
about it.
- Remove the cli command ``neurom features`` that listed all possible features. Instead a proper
documentation is provided on that topic. See :func:`neurom.features.get`.
- Make ``neurom.features.neuronfunc.sholl_crossings`` private.

Version 2.2.1
-------------
1 change: 1 addition & 0 deletions doc/source/api.rst
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ API Documentation

neurom.morphmath
neurom.features
neurom.features.neuronfunc
neurom.features.neuritefunc
neurom.features.sectionfunc
neurom.features.bifurcationfunc
4 changes: 2 additions & 2 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
@@ -140,11 +140,11 @@
todo_include_todos = True
suppress_warnings = ["ref.python"]
autosummary_generate = True
autosummary_imported_members = True
autosummary_imported_members = False
autoclass_content = 'both'
autodoc_default_options = {
'members': True,
'imported-members': True,
'imported-members': False,
'show-inheritance': True,
}

7 changes: 2 additions & 5 deletions doc/source/morph_stats.rst
Original file line number Diff line number Diff line change
@@ -125,8 +125,5 @@ or ``-h`` option.
Features
--------

To see all available features for ``--config``:

.. runblock:: console

$ neurom features
All available features for ``--config`` are documented in :mod:`neurom.features.neuronfunc` and
:mod:`neurom.features.neuritefunc`.
10 changes: 0 additions & 10 deletions neurom/apps/cli.py
Original file line number Diff line number Diff line change
@@ -32,7 +32,6 @@
import click
import matplotlib.pyplot as plt

import neurom as nm
from neurom.apps import morph_stats, morph_check
from neurom import load_neuron
from neurom.viewer import draw as pyplot_draw
@@ -94,15 +93,6 @@ def stats(datapath, config, output, full_config, as_population, ignored_exceptio
morph_stats.main(datapath, config, output, full_config, as_population, ignored_exceptions)


@cli.command(short_help='list all available features')
def features():
"""Cli to get list of available features. For backward compatibility."""
# TODO replace it with programmatically generated Sphinx page that contains all available
# features, also programmatically generate EXAMPLE_CONFIG on that page.
# pylint: disable=protected-access
print(nm.features._get_doc())


@cli.command(short_help='Perform checks on morphologies, more details at'
'https://neurom.readthedocs.io/en/latest/morph_check.html')
@click.argument('datapath')
39 changes: 5 additions & 34 deletions neurom/features/__init__.py
Original file line number Diff line number Diff line change
@@ -99,9 +99,13 @@ def _get_feature_value_and_func(feature_name, obj, **kwargs):
def get(feature_name, obj, **kwargs):
"""Obtain a feature from a set of morphology objects.
Features can be either Neurite features or Neuron features. For the list of Neurite features
see :mod:`neurom.features.neuritefunc`. For the list of Neuron features see
:mod:`neurom.features.neuronfunc`.
Arguments:
feature_name(string): feature to extract
obj: a neuron, population or neurite tree
obj: a neuron, a neuron population or a neurite tree
kwargs: parameters to forward to underlying worker functions
Returns:
@@ -110,36 +114,6 @@ def get(feature_name, obj, **kwargs):
return _get_feature_value_and_func(feature_name, obj, **kwargs)[0]


_INDENT = ' ' * 4


def _indent(string, count):
"""Indent `string` by `count` * INDENT."""
indent = _INDENT * count
ret = indent + string.replace('\n', '\n' + indent)
return ret.rstrip()


def _get_doc():
"""Get a description of all the known available features."""
def get_docstring(func):
"""Extract doctstring, if possible."""
docstring = ':\n'
if func.__doc__:
docstring += _indent(func.__doc__, 2)
return docstring

ret = ['\nNeurite features (neurite, neuron, neuron population):']
ret.extend(_INDENT + '- ' + feature + get_docstring(func)
for feature, func in sorted(NEURITEFEATURES.items()))

ret.append('\nNeuron features (neuron, neuron population):')
ret.extend(_INDENT + '- ' + feature + get_docstring(func)
for feature, func in sorted(NEURONFEATURES.items()))

return '\n'.join(ret)


def _register_feature(namespace, name, func, shape):
"""Register a feature to be applied.
@@ -178,6 +152,3 @@ def inner(func):

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

# This must be done after all features have been registered
get.__doc__ += _indent('\nFeatures:\n', 1) + _indent(_get_doc(), 2) # pylint: disable=no-member
32 changes: 21 additions & 11 deletions neurom/features/neuritefunc.py
Original file line number Diff line number Diff line change
@@ -26,7 +26,17 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Neurite functions."""
"""Neurite features.
Any public function from this namespace can be called via features mechanism on a neurite, a
collection of neurites, a neuron, a neuron population:
>>> import neurom
>>> from neurom import features
>>> nrn = neurom.load_neuron('path/to/neuron')
>>> features.get('max_radial_distance', nrn.neurites)
>>> features.get('n_segments', nrn.neurites, neurite_type=neurom.AXON)
"""

import logging
from functools import partial, update_wrapper
@@ -39,7 +49,7 @@
from neurom.core.dataformat import COLS
from neurom.core.types import tree_type_checker as is_type
from neurom.features import _register_feature, bifurcationfunc, feature, sectionfunc
from neurom.features.neuronfunc import neuron_population
from neurom.features.neuronfunc import _neuron_population
from neurom.geom import convex_hull
from neurom.morphmath import interval_lengths

@@ -173,52 +183,52 @@ def pl2(node):
# Features returning one value per NEURON #
################################################################################

def map_neurons(fun, neurites, neurite_type):
def _map_neurons(fun, neurites, neurite_type):
"""Map `fun` to all the neurites in a single or collection of neurons."""
nrns = neuron_population(neurites)
nrns = _neuron_population(neurites)
return [fun(n, neurite_type=neurite_type) for n in nrns]


@feature(shape=(...,))
def max_radial_distances(neurites, neurite_type=NeuriteType.all):
"""Get the maximum radial distances of the termination sections for a collection of neurites."""
return map_neurons(max_radial_distance, neurites, neurite_type)
return _map_neurons(max_radial_distance, neurites, neurite_type)


@feature(shape=(...,))
def number_of_sections(neurites, neurite_type=NeuriteType.all):
"""Number of sections in a collection of neurites."""
return map_neurons(n_sections, neurites, neurite_type)
return _map_neurons(n_sections, neurites, neurite_type)


@feature(shape=(...,))
def number_of_neurites(neurites, neurite_type=NeuriteType.all):
"""Number of neurites in a collection of neurites."""
return map_neurons(n_neurites, neurites, neurite_type)
return _map_neurons(n_neurites, neurites, neurite_type)


@feature(shape=(...,))
def number_of_bifurcations(neurites, neurite_type=NeuriteType.all):
"""Number of bifurcation points in a collection of neurites."""
return map_neurons(n_bifurcation_points, neurites, neurite_type)
return _map_neurons(n_bifurcation_points, neurites, neurite_type)


@feature(shape=(...,))
def number_of_forking_points(neurites, neurite_type=NeuriteType.all):
"""Number of forking points in a collection of neurites."""
return map_neurons(n_forking_points, neurites, neurite_type)
return _map_neurons(n_forking_points, neurites, neurite_type)


@feature(shape=(...,))
def number_of_terminations(neurites, neurite_type=NeuriteType.all):
"""Number of leaves points in a collection of neurites."""
return map_neurons(n_leaves, neurites, neurite_type)
return _map_neurons(n_leaves, neurites, neurite_type)


@feature(shape=(...,))
def number_of_segments(neurites, neurite_type=NeuriteType.all):
"""Number of sections in a collection of neurites."""
return map_neurons(n_segments, neurites, neurite_type)
return _map_neurons(n_segments, neurites, neurite_type)

################################################################################
# Features returning one value per SEGMENT #
60 changes: 37 additions & 23 deletions neurom/features/neuronfunc.py
Original file line number Diff line number Diff line change
@@ -26,7 +26,19 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Morphometrics functions for neurons or neuron populations."""
"""Neuron features.
Any public function from this namespace can be called via features mechanism on a neuron, a
neuron population:
>>> import neurom
>>> from neurom import features
>>> nrn = neurom.load_neuron('path/to/neuron')
>>> features.get('soma_surface_area', nrn)
>>> nrn_population = neurom.load_neurons('path/to/neurons')
>>> features.get('sholl_frequency', nrn_population)
"""


from functools import partial
import math
@@ -43,7 +55,7 @@
feature = partial(feature, namespace='NEURONFEATURES')


def neuron_population(nrns):
def _neuron_population(nrns):
"""Makes sure `nrns` behaves like a neuron population."""
return nrns.neurons if hasattr(nrns, 'neurons') else (nrns,)

@@ -62,7 +74,7 @@ def soma_volumes(nrn_pop):
If a single neuron is passed, a single element list with the volume
of its soma member is returned.
"""
nrns = neuron_population(nrn_pop)
nrns = _neuron_population(nrn_pop)
return [soma_volume(n) for n in nrns]


@@ -88,7 +100,7 @@ def soma_surface_areas(nrn_pop, neurite_type=NeuriteType.soma):
If a single neuron is passed, a single element list with the surface
area of its soma member is returned.
"""
nrns = neuron_population(nrn_pop)
nrns = _neuron_population(nrn_pop)
assert neurite_type == NeuriteType.soma, 'Neurite type must be soma'
return [soma_surface_area(n) for n in nrns]

@@ -102,7 +114,7 @@ def soma_radii(nrn_pop, neurite_type=NeuriteType.soma):
radius of its soma member is returned.
"""
assert neurite_type == NeuriteType.soma, 'Neurite type must be soma'
nrns = neuron_population(nrn_pop)
nrns = _neuron_population(nrn_pop)
return [n.soma.radius for n in nrns]


@@ -131,7 +143,7 @@ def trunk_origin_azimuths(nrn, neurite_type=NeuriteType.all):
The range of the azimuth angle [-pi, pi] radians
"""
neurite_filter = is_type(neurite_type)
nrns = neuron_population(nrn)
nrns = _neuron_population(nrn)

def _azimuth(section, soma):
"""Azimuth of a section."""
@@ -154,7 +166,7 @@ def trunk_origin_elevations(nrn, neurite_type=NeuriteType.all):
The range of the elevation angle [-pi/2, pi/2] radians
"""
neurite_filter = is_type(neurite_type)
nrns = neuron_population(nrn)
nrns = _neuron_population(nrn)

def _elevation(section, soma):
"""Elevation of a section."""
@@ -174,7 +186,7 @@ def _elevation(section, soma):
def trunk_vectors(nrn, neurite_type=NeuriteType.all):
"""Calculates the vectors between all the trunks of the neuron and the soma center."""
neurite_filter = is_type(neurite_type)
nrns = neuron_population(nrn)
nrns = _neuron_population(nrn)

return np.array([morphmath.vector(s.root_node.points[0], n.soma.center)
for n in nrns
@@ -209,24 +221,25 @@ def _sort_angle(p1, p2):
for i, _ in enumerate(ordered_vectors)]


def sholl_crossings(neurites, center, radii, neurite_filter=None):
"""Calculate crossings of neurites.
This function can also be used with a list aa neurites, as follow:
secs = (sec for sec in nm.iter_sections(neuron) if complex_filter(sec))
sholl = nm.features.neuronfunc.sholl_crossings(secs,
center=neuron.soma.center,
radii=np.arange(0, 1000, 100))
def sholl_crossings(neurites, center, radii, neurite_type=NeuriteType.all):
"""Calculate crossings of neurites. The only function in this module that is not a feature.
Args:
neurites(list): morphology on which to perform Sholl analysis, or list of neurites
center(Point): center point
radii(iterable of floats): radii for which crossings will be counted
neurite_type(NeuriteType): Type of neurite to use. By default ``NeuriteType.all`` is used.
Returns:
Array of same length as radii, with a count of the number of crossings
for the respective radius
This function can also be used with a list of sections, as follow::
secs = (sec for sec in nm.iter_sections(neuron) if complex_filter(sec))
sholl = nm.features.neuritefunc.sholl_crossings(secs,
center=neuron.soma.center,
radii=np.arange(0, 1000, 100))
"""
def _count_crossings(neurite, radius):
"""Used to count_crossings of segments in neurite with radius."""
@@ -242,7 +255,7 @@ def _count_crossings(neurite, radius):
return count

return np.array([sum(_count_crossings(neurite, r)
for neurite in iter_neurites(neurites, filt=neurite_filter))
for neurite in iter_neurites(neurites, filt=is_type(neurite_type)))
for r in radii])


@@ -266,20 +279,21 @@ def sholl_frequency(nrn, neurite_type=NeuriteType.all, step_size=10, bins=None):
bends back on itself, and crosses the same Sholl radius will get counted as
having crossed multiple times.
"""
nrns = neuron_population(nrn)
neurite_filter = is_type(neurite_type)
nrns = _neuron_population(nrn)

if bins is None:
min_soma_edge = min(neuron.soma.radius for neuron in nrns)
max_radii = max(np.max(np.linalg.norm(neurite.points[:, COLS.XYZ], axis=1))
for nrn in nrns for neurite in iter_neurites(nrn, filt=neurite_filter))
for nrn in nrns
for neurite in iter_neurites(nrn, filt=is_type(neurite_type)))
bins = np.arange(min_soma_edge, min_soma_edge + max_radii, step_size)

return sum(sholl_crossings(neuron, neuron.soma.center, bins, neurite_filter) for neuron in nrns)
return sum(sholl_crossings(neuron, neuron.soma.center, bins, neurite_type)
for neuron in nrns)


@feature(shape=(...,))
def total_length(nrn_pop, neurite_type=NeuriteType.all):
"""Get the total length of all sections in the group of neurons or neurites."""
nrns = neuron_population(nrn_pop)
nrns = _neuron_population(nrn_pop)
return list(sum(neuritefunc.section_lengths(n, neurite_type=neurite_type)) for n in nrns)
9 changes: 1 addition & 8 deletions tests/apps/test_cli.py
Original file line number Diff line number Diff line change
@@ -72,7 +72,7 @@ def test_morph_stat_full_config():
runner = CliRunner()
filename = DATA / 'h5/v1/Neuron.h5'
with tempfile.NamedTemporaryFile() as f:
result = runner.invoke(cli, ['stats', str(filename), '--full-config', '--output', f.name])
result = runner.invoke(cli, ['stats', str(filename), '--full-config', '--output', f.name], catch_exceptions=False)
assert result.exit_code == 0
df = pd.read_csv(f)
assert not df.empty
@@ -129,10 +129,3 @@ def test_morph_check():
'Has nonzero soma radius': True,
'ALL': False}},
'STATUS': 'FAIL'}


def test_features():
runner = CliRunner()
result = runner.invoke(cli, ['features'])
assert result.exit_code == 0
assert len(result.stdout) > 0
Loading

0 comments on commit 8d2fc96

Please sign in to comment.