Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Ensure all features return built-in types #1064

Merged
merged 2 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions neurom/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
import inspect
import operator
from enum import Enum
from functools import partial, reduce
from functools import partial, reduce, wraps

import numpy as np

from neurom.core import Morphology, Neurite, Population
from neurom.core.morphology import iter_neurites
Expand Down Expand Up @@ -226,15 +228,35 @@ def _register_feature(namespace: NameSpace, name, func, shape):
def feature(shape, namespace: NameSpace, name=None):
"""Feature decorator to automatically register the feature in the appropriate namespace.

This decorator also ensures that the results of the features are casted to built-in types.

Arguments:
shape(tuple): the expected shape of the feature values
namespace(string): a namespace, see :class:`NameSpace`
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
@wraps(func)
def scalar_wrapper(*args, **kwargs):
res = func(*args, **kwargs)
try:
return res.tolist()
except AttributeError:
return res

@wraps(func)
def matrix_wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return np.array(res).tolist()

if shape == ():
decorated_func = scalar_wrapper
else:
decorated_func = matrix_wrapper

_register_feature(namespace, name or func.__name__, decorated_func, shape)
return decorated_func

return inner

Expand Down
98 changes: 98 additions & 0 deletions tests/features/test_features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Miscelaneous tests of features."""
from pathlib import Path
from itertools import chain

import numpy as np
import pytest
from numpy import testing as npt

import neurom as nm
from neurom import features


@pytest.fixture
def DATA_PATH():
return Path(__file__).parent.parent / "data"


@pytest.fixture
def SWC_PATH(DATA_PATH):
return DATA_PATH / "swc"


@pytest.fixture
def MORPHOLOGY(SWC_PATH):
return nm.load_morphology(SWC_PATH / "test_morph.swc")


@pytest.fixture
def NEURITE(MORPHOLOGY):
return MORPHOLOGY.neurites[0]


@pytest.fixture
def SECTION(NEURITE):
return NEURITE.sections[0]


@pytest.fixture
def NRN_FILES(DATA_PATH):
return [
DATA_PATH / "h5/v1" / f for f in ("Neuron.h5", "Neuron_2_branch.h5", "bio_neuron-001.h5")
]


@pytest.fixture
def POP(NRN_FILES):
return nm.load_morphologies(NRN_FILES)


def _check_nested_type(data):
"""Check that the given data contains only built-in types.

The data should either be an int or float, or a list or tuple of ints or floats.
"""
if isinstance(data, (list, tuple)):
for i in data:
_check_nested_type(i)
else:
assert isinstance(data, (int, float))


@pytest.mark.parametrize(
"feature_name",
[
pytest.param(name, id=f"Test type of {name} neurite feature")
for name in features._NEURITE_FEATURES
],
)
def test_neurite_feature_types(feature_name, NEURITE):
"""Test neurite features."""
res = features._NEURITE_FEATURES.get(feature_name)(NEURITE)
_check_nested_type(res)


@pytest.mark.parametrize(
"feature_name",
[
pytest.param(name, id=f"Test type of {name} morphology feature")
for name in features._MORPHOLOGY_FEATURES
],
)
def test_morphology_feature_types(feature_name, MORPHOLOGY):
"""Test morphology features."""
res = features._MORPHOLOGY_FEATURES.get(feature_name)(MORPHOLOGY)
_check_nested_type(res)


@pytest.mark.parametrize(
"feature_name",
[
pytest.param(name, id=f"Test type of {name} population feature")
for name in features._POPULATION_FEATURES
],
)
def test_population_feature_types(feature_name, POP):
"""Test population features."""
res = features._POPULATION_FEATURES.get(feature_name)(POP)
_check_nested_type(res)
2 changes: 1 addition & 1 deletion tests/features/test_neurite.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def test_section_end_distances():

def test_section_partition_pairs():
part_pairs = [neurite.partition_pairs(s) for s in SIMPLE.neurites]
assert part_pairs == [[(1.0, 1.0)], [(1.0, 1.0)]]
assert part_pairs == [[[1.0, 1.0]], [[1.0, 1.0]]]


def test_section_bif_radial_distances():
Expand Down