Repository: BlueBrain/NeuroM

Issue:
`sholl_frequency` fails if `neurite_type` not in morphology
```python
import neurom
m = neurom.load_morphology(
    """
    1  1    0  0  0    0.5 -1
    2  3   -1  0  0    0.1  1
    3  3   -2  0  0    0.1  2
    4  3   -3  0  0    0.1  3
    """",
    reader="swc"
)
neurom.get("sholl_frequency", step_size=1, neurite_type=neurom.NeuriteType.axon)
```
throws:
```python
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [40], in <module>
----> 1 get("sholl_frequency", m, step_size=1, neurite_type=neurom.NeuriteType.axon)

File ~/Projects/BBP/NEUROM/venv3.8-test/lib/python3.8/site-packages/neurom/features/__init__.py:150, in get(feature_name, obj, **kwargs)
    135 def get(feature_name, obj, **kwargs):
    136     """Obtain a feature from a set of morphology objects.
    137 
    138     Features can be either Neurite, Morphology or Population features. For Neurite features see
   (...)
    148         List|Number: feature value as a list or a single number.
    149     """
--> 150     return _get_feature_value_and_func(feature_name, obj, **kwargs)[0]

File ~/Projects/BBP/NEUROM/venv3.8-test/lib/python3.8/site-packages/neurom/features/__init__.py:110, in _get_feature_value_and_func(feature_name, obj, **kwargs)
    108 if feature_name in _MORPHOLOGY_FEATURES:
    109     feature_ = _MORPHOLOGY_FEATURES[feature_name]
--> 110     res = feature_(obj, **kwargs)
    111 elif feature_name in _NEURITE_FEATURES:
    112     feature_ = _NEURITE_FEATURES[feature_name]

File ~/Projects/BBP/NEUROM/venv3.8-test/lib/python3.8/site-packages/neurom/features/morphology.py:293, in sholl_frequency(morph, neurite_type, step_size, bins)
    291 if bins is None:
    292     min_soma_edge = morph.soma.radius
--> 293     max_radii = max(np.max(np.linalg.norm(n.points[:, COLS.XYZ], axis=1))
    294                     for n in morph.neurites if neurite_filter(n))
    295     bins = np.arange(min_soma_edge, min_soma_edge + max_radii, step_size)
    297 return sholl_crossings(morph, morph.soma.center, bins, neurite_type)

ValueError: max() arg is an empty sequence
```
I can think of two possible solutions here:
1. Return a [0] bin count.
2. Raise a meaningful error which will inform the user that the `neurite_type` provided is not present in the morphology

Note: This error takes place only when bins are not provided. It tries to get the max of the points that belong to neurites of that type but finds none.

@lidakanari @arnaudon @adrien-berchet 

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/BlueBrain/NeuroM/pull/990?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BlueBrain) Report
> Merging [#990](https://codecov.io/gh/BlueBrain/NeuroM/pull/990?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BlueBrain) (13646db) into [master](https://codecov.io/gh/BlueBrain/NeuroM/commit/d120263701bd2de8fb6f8ea6c655b9ec8a0a99ce?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=BlueBrain) (d120263) will **not change** coverage.
> The diff coverage is `100.00%`.



```diff
@@            Coverage Diff            @@
##            master      #990   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           36        36           
  Lines         2332      2334    +2     
=========================================
+ Hits          2332      2334    +2     
```

eleftherioszisis: Good point, I added this case in the `Note` section of the docstring.
adrien-berchet: LGTM!

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f852163..de024a1 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -3,10 +3,18 @@ Changelog
 
 Version 3.2.0
 -------------
+- Fix ``neurom.features.morphology.sholl_frequency`` to return an empty list when a
+  neurite_type that is not present in the morphology is specified.
+- Fix ``neurom.features.morphology.trunk_origin_radii`` to warn and use only the root
+  section for the calculation of the path distances. Edge cases from the combination
+  of ``min_length_filter`` and ``max_length_filter`` are addressed.
+- Fix ``neurom.features.morphology.sholl_frequency`` to use soma center in distance
+  calculation, instead of using the origin.
 - Add ``neurom.features.morphology.trunk_angles_inter_types`` and
   ``neurom.features.morphology.trunk_angles_from_vector`` features, make
   ``neurom.features.morphology.trunk_angles`` more generic and add length filter to
   ``neurom.features.morphology.trunk_origin_radii``.
+- Deprecate python3.6
 - Add doc on spherical coordinates.
 
 Version 3.1.0
diff --git a/neurom/features/morphology.py b/neurom/features/morphology.py
index 405816d..2aded0e 100644
--- a/neurom/features/morphology.py
+++ b/neurom/features/morphology.py
@@ -515,16 +515,24 @@ def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None
         in steps of `step_size`. Each segment of the morphology is tested, so a neurite that
         bends back on itself, and crosses the same Sholl radius will get counted as
         having crossed multiple times.
+
+        If a `neurite_type` is specified and there are no trees corresponding to it, an empty
+        list will be returned.
     """
     neurite_filter = is_type(neurite_type)
 
     if bins is None:
         min_soma_edge = morph.soma.radius
-        max_radii = max(
+
+        max_radius_per_neurite = [
             np.max(np.linalg.norm(n.points[:, COLS.XYZ] - morph.soma.center, axis=1))
             for n in morph.neurites if neurite_filter(n)
-        )
-        bins = np.arange(min_soma_edge, min_soma_edge + max_radii, step_size)
+        ]
+
+        if not max_radius_per_neurite:
+            return []
+
+        bins = np.arange(min_soma_edge, min_soma_edge + max(max_radius_per_neurite), step_size)
 
     return sholl_crossings(morph, neurite_type, morph.soma.center, bins)
 


Test patch:
diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py
index 312edaf..61d3119 100644
--- a/tests/features/test_get_features.py
+++ b/tests/features/test_get_features.py
@@ -717,6 +717,10 @@ def test_sholl_frequency():
 
     assert features.get('sholl_frequency', m, step_size=5.0) == [0, 1, 1, 1]
 
+    # check that if there is no neurite of a specific type, an empty list is returned
+    assert features.get('sholl_frequency', m, neurite_type=NeuriteType.axon) == []
+
+
 def test_bifurcation_partitions():
     assert_allclose(features.get('bifurcation_partitions', POP)[:10],
                     [19., 17., 15., 13., 11., 9., 7., 5., 3., 1.])