diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py index f22d2f67..b415f77a 100644 --- a/neurom/features/morphology.py +++ b/neurom/features/morphology.py @@ -762,7 +762,7 @@ def shape_factor(morph, neurite_type=NeuriteType.all, projection_plane="xy", use @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: @@ -779,7 +779,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 4884ff70..7c972779 100644 --- a/neurom/features/neurite.py +++ b/neurom/features/neurite.py @@ -188,14 +188,10 @@ def section_term_branch_orders(neurite, section_type=NeuriteType.all): @feature(shape=(...,)) def section_path_distances(neurite, iterator_type=Section.ipreorder, section_type=NeuriteType.all): """Path lengths.""" - - def path_length(node): - """Calculate the path length using cached section lengths.""" - sections = utils.takeuntil(lambda s: s.id == neurite.root_node.id, node.iupstream()) - return sum(n.length for n in sections) - return _map_sections( - path_length, neurite, iterator_type=iterator_type, section_type=section_type + partial(sf.section_path_length, stop_node=neurite.root_node), + neurite, + iterator_type=iterator_type, section_type=section_type ) diff --git a/neurom/features/section.py b/neurom/features/section.py index 6072e9ef..9809d495 100644 --- a/neurom/features/section.py +++ b/neurom/features/section.py @@ -35,6 +35,7 @@ from neurom.core.morphology import iter_segments from neurom.core.morphology import Section from neurom.morphmath import interval_lengths +from neurom import utils def section_points(section): @@ -42,9 +43,19 @@ 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. + """ + it = section.iupstream() + + if stop_node: + it = utils.takeuntil(lambda s: s.id == stop_node.id, it) + + return sum(map(section_length, it)) def section_length(section): diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 8522fab9..a9075500 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -10,15 +10,59 @@ from neurom.features import _POPULATION_FEATURES, _MORPHOLOGY_FEATURES, _NEURITE_FEATURES import collections.abc -from neurom.core import morphology as tested +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 - apical_dendrite: homogeneous + 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( """ @@ -73,11 +117,11 @@ def test_homogeneous_subtrees(mixed_morph): basal, axon_on_basal, apical = mixed_morph.neurites - assert tested._homogeneous_subtrees(basal) == [basal] + assert neurom.core.morphology._homogeneous_subtrees(basal) == [basal] sections = list(axon_on_basal.iter_sections()) - subtrees = tested._homogeneous_subtrees(axon_on_basal) + 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 @@ -88,14 +132,14 @@ def test_homogeneous_subtrees(mixed_morph): def test_iter_neurites__heterogeneous(mixed_morph): - subtrees = list(tested.iter_neurites(mixed_morph, use_subtrees=False)) + 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(tested.iter_neurites(mixed_morph, use_subtrees=True)) + subtrees = list(neurom.core.morphology.iter_neurites(mixed_morph, use_subtrees=True)) assert len(subtrees) == 4 assert subtrees[0].type == NeuriteType.basal_dendrite @@ -104,6 +148,74 @@ def test_iter_neurites__heterogeneous(mixed_morph): 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]) @@ -778,7 +890,19 @@ def _morphology_features(mode): "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())