Repository: BlueBrain/NeuroM

Issue:
Maximal direction extent
@lidakanari , asked for a maximal direction extent feature in https://github.com/BlueBrain/NeuroM/pull/1008#issuecomment-1073704148

There are two ways to achieve this:

1. Add a new feature that will return the maximum of the principal direction extents.
2. Order the existing principal_direction_extent feature from max to min. The max extent would then be equal to `principal_direction_extent(direction=0)`

@arnaudon , @adrien-berchet, @mgeplf 

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 1a4feec..8606975 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -4,6 +4,8 @@ Changelog
 Version 3.2.0
 -------------
 
+- ``neurom.features.neurite.principal_direction_extents`` directions correspond to extents
+  ordered in a descending order.
 - Add features ``neurom.features.morphology.(aspect_ration, circularity, shape_factor)```
 - Fix ``neurom.morphmath.principal_direction_extent`` to calculate correctly the pca extent.
 - Fix ``neurom.features.neurite.segment_taper_rates`` to return signed taper rates.
diff --git a/neurom/features/neurite.py b/neurom/features/neurite.py
index 0439d88..6aac5e9 100644
--- a/neurom/features/neurite.py
+++ b/neurom/features/neurite.py
@@ -446,7 +446,12 @@ def section_end_distances(neurite):
 
 @feature(shape=(...,))
 def principal_direction_extents(neurite, direction=0):
-    """Principal direction extent of neurites in morphologies."""
+    """Principal direction extent of neurites in morphologies.
+
+    Note:
+        Principal direction extents are always sorted in descending order. Therefore,
+        by default the maximal principal direction extent is returned.
+    """
     return [morphmath.principal_direction_extent(neurite.points[:, COLS.XYZ])[direction]]
 
 
diff --git a/neurom/morphmath.py b/neurom/morphmath.py
index 248c2e9..45f0700 100644
--- a/neurom/morphmath.py
+++ b/neurom/morphmath.py
@@ -479,7 +479,7 @@ def principal_direction_extent(points):
         extents : the extents for each of the eigenvectors of the cov matrix
 
     Note:
-        Direction extents are not ordered from largest to smallest.
+        Direction extents are ordered from largest to smallest.
     """
     # pca can be biased by duplicate points
     points = np.unique(points, axis=0)
@@ -488,13 +488,17 @@ def principal_direction_extent(points):
     points -= np.mean(points, axis=0)
 
     # principal components
-    _, eigv = pca(points)
+    _, eigenvectors = pca(points)
 
     # for each eigenvector calculate the scalar projection of the points on it (n_points, n_eigv)
-    scalar_projections = points.dot(eigv)
+    scalar_projections = points.dot(eigenvectors)
 
-    # and return the range of the projections (abs(max - min)) along each column (eigenvector)
-    return np.ptp(scalar_projections, axis=0)
+    # range of the projections (abs(max - min)) along each column (eigenvector)
+    extents = np.ptp(scalar_projections, axis=0)
+
+    descending_order = np.argsort(extents)[::-1]
+
+    return extents[descending_order]
 
 
 def convex_hull(points):


Test patch:
diff --git a/tests/features/test_get_features.py b/tests/features/test_get_features.py
index 6b480c8..20372b9 100644
--- a/tests/features/test_get_features.py
+++ b/tests/features/test_get_features.py
@@ -798,18 +798,46 @@ def test_principal_direction_extents():
 
     # test with a realistic morphology
     m = nm.load_morphology(DATA_PATH / 'h5/v1' / 'bio_neuron-000.h5')
-    p_ref = [
-        1210.569727,
-        38.493958,
-        147.098687,
-        288.226628,
-        330.166506,
-        152.396521,
-        293.913857
-    ]
-    p = features.get('principal_direction_extents', m)
-    assert_allclose(p, p_ref, rtol=1e-6)
 
+    assert_allclose(
+        features.get('principal_direction_extents', m, direction=0),
+        [
+            1210.569727,
+            117.988454,
+            147.098687,
+            288.226628,
+            330.166506,
+            152.396521,
+            293.913857,
+        ],
+        atol=1e-6
+    )
+    assert_allclose(
+        features.get('principal_direction_extents', m, direction=1),
+        [
+            851.730088,
+            99.108911,
+            116.949436,
+            157.171734,
+            137.328019,
+            20.66982,
+            67.157249,
+        ],
+        atol=1e-6
+    )
+    assert_allclose(
+        features.get('principal_direction_extents', m, direction=2),
+        [
+            282.961199,
+            38.493958,
+            40.715183,
+            94.061625,
+            51.120255,
+            10.793167,
+            62.808188
+        ],
+        atol=1e-6
+    )
 
 def test_total_width():
 
diff --git a/tests/test_morphmath.py b/tests/test_morphmath.py
index 181aaf8..43bb4dc 100644
--- a/tests/test_morphmath.py
+++ b/tests/test_morphmath.py
@@ -587,8 +587,8 @@ def test_principal_direction_extent():
     ])
 
     npt.assert_allclose(
-        sorted(mm.principal_direction_extent(cross_3D_points)),
-        [6.0, 10.0, 12.0], atol=0.1,
+        mm.principal_direction_extent(cross_3D_points),
+        [12.0, 10.0, 6.0], atol=0.1,
     )