In [1]:
# A few tools for downloading the data
from io import StringIO
import requests
from neuron_morphology.swc_io import morphology_from_swc
from neuron_morphology.feature_extractor.data import Data
In [2]:
def data_from_url(morphology_url):
    morphology_swc = StringIO(requests.get(morphology_url).text)

    # Feature functions expect a Data object - in this case just a wrapper for a Morphology
    # If we were working with additional data (say, layer annotations) we would store these here as well
    return Data(morphology_from_swc(morphology_swc))

# fetch a published reconstruction 
test_data = data_from_url("http://celltypes.brain-map.org/api/v2/well_known_file_download/491120375")
more_test_data = data_from_url("http://celltypes.brain-map.org/api/v2/well_known_file_download/692297222")

Plot it real quick¶

In [3]:
import matplotlib.pyplot as plt

nodes = test_data.morphology.nodes()
x = [node['x'] for node in nodes]
y = [node['y'] for node in nodes]
z = [node['z'] for node in nodes]

fig, ax = plt.subplots(1, 2)
ax[0].scatter(x, y, s=0.1)
ax[1].scatter(z, y, s=0.1)
Out[3]:
<matplotlib.collections.PathCollection at 0x7fdf773e3a50>

neuron_morphology provides a library of feature functions¶

See the features subpackage for the full list.

In [4]:
from neuron_morphology.features.branching.bifurcations import num_outer_bifurcations

num_outer_bifurcations
Out[4]:
Signature:
num_outer_bifurcations(data: neuron_morphology.feature_extractor.data.Data, node_types: Union[List[int], NoneType] = None) -> int

Marks: ['RequiresRoot', 'BifurcationFeatures']

Requires: frozenset()

Help:
 Feature Extractor interface to calculate_outer_bifurcations. Returns the 
    number of bifurcations (branch points), excluding those too close to the 
    root (threshold is 1/2 the max distance from the root to any node).

    Parameters
    ----------
    data : Holds a morphology object. No additional data is required
    node_types : Restrict included nodes to these types. See 
        neuron_morphology.constants for avaiable node types. 

    
In [5]:
# we can call this just like any function
num_outer_bifurcations(test_data)
Out[5]:
6

You can look in default features for a list of the features that are run by default.

Use a FeatureExtractor to run many features at once¶

In [7]:
from neuron_morphology.feature_extractor.feature_extractor import FeatureExtractor
from neuron_morphology.features.dimension import dimension
from neuron_morphology.features.intrinsic import num_branches, num_tips

# a utility for flattening outputs
from neuron_morphology.feature_extractor.utilities import unnest


features = [
    num_outer_bifurcations,
    dimension,
    num_branches,
    num_tips
]

# make a pipeline
results = (
    FeatureExtractor()
    .register_features(features)
    .extract(test_data)
    .results
)

unnest(results)
/home/nile/Desktop/neuron_morphology/neuron_morphology/feature_extractor/mark.py:118: UserWarning: This morphology is not uniquely rooted! Found 7 root nodes. Features using the root node of this morphology may not select that node consistently. Some or all of these root nodes may not be soma nodes.
  f"This morphology is not uniquely rooted! Found {num_roots} "
Out[7]:
{'num_outer_bifurcations': 6,
 'dimension.width': 598.6509,
 'dimension.height': 602.6546,
 'dimension.depth': 71.5631,
 'dimension.min_xyz': array([-252.2122, -220.8229,  -16.1489]),
 'dimension.max_xyz': array([346.4387, 381.8317,  55.4142]),
 'dimension.bias_xyz': array([ 94.2265, 161.0088,  39.2653]),
 'num_branches': 234,
 'num_tips': 122}

Use specialization to run features across neurite types¶

... or other parameters

In [8]:
from neuron_morphology.feature_extractor.feature_specialization import NEURITE_SPECIALIZATIONS, AxonSpec
from neuron_morphology.feature_extractor.marked_feature import specialize
In [9]:
print(f"marks: {AxonSpec.marks}")
print(f"kwargs: {AxonSpec.kwargs}")
marks: {<class 'neuron_morphology.feature_extractor.mark.RequiresAxon'>}
kwargs: {'node_types': [2]}
In [10]:
axon_num_branches = specialize(num_branches, {AxonSpec}) # note that this is a set!
unnest(
    FeatureExtractor()
    .register_features([axon_num_branches])
    .extract(test_data)
    .results
)
Out[10]:
{'axon.num_branches': 205}

Running on an axon is not super interesting, we can pass in a whole set of specializations, such as the built-in NEURITE_SPECIALIZATIONS to distribute a calculation across many parameter values.

In [11]:
NEURITE_SPECIALIZATIONS
Out[11]:
{neuron_morphology.feature_extractor.feature_specialization.AllNeuriteSpec,
 neuron_morphology.feature_extractor.feature_specialization.ApicalDendriteSpec,
 neuron_morphology.feature_extractor.feature_specialization.AxonSpec,
 neuron_morphology.feature_extractor.feature_specialization.BasalDendriteSpec,
 neuron_morphology.feature_extractor.feature_specialization.DendriteSpec}
In [12]:
across_neurites = [specialize(feature, NEURITE_SPECIALIZATIONS) for feature in features]

# this syntax is (almost) equivalent to the above pipeline, but we keep the extractor around
extractor = FeatureExtractor()
extractor.register_features(across_neurites)
run = extractor.extract(test_data)
across_neurite_results = run.results

unnest(across_neurite_results)
Out[12]:
{'all_neurites.num_outer_bifurcations': 6,
 'dendrite.num_outer_bifurcations': 2,
 'basal_dendrite.num_outer_bifurcations': 2,
 'axon.num_outer_bifurcations': 4,
 'all_neurites.dimension.width': 598.6509,
 'all_neurites.dimension.height': 602.6546,
 'all_neurites.dimension.depth': 71.5631,
 'all_neurites.dimension.min_xyz': array([-252.2122, -220.8229,  -16.1489]),
 'all_neurites.dimension.max_xyz': array([346.4387, 381.8317,  55.4142]),
 'all_neurites.dimension.bias_xyz': array([ 94.2265, 161.0088,  39.2653]),
 'dendrite.dimension.width': 384.2443999999999,
 'dendrite.dimension.height': 209.35199999999998,
 'dendrite.dimension.depth': 58.8211,
 'dendrite.dimension.min_xyz': array([ -86.8776, -151.2368,  -15.5191]),
 'dendrite.dimension.max_xyz': array([297.3668,  58.1152,  43.302 ]),
 'dendrite.dimension.bias_xyz': array([210.4892,  93.1216,  27.7829]),
 'basal_dendrite.dimension.width': 384.2443999999999,
 'basal_dendrite.dimension.height': 209.35199999999998,
 'basal_dendrite.dimension.depth': 58.8211,
 'basal_dendrite.dimension.min_xyz': array([ -86.8776, -151.2368,  -15.5191]),
 'basal_dendrite.dimension.max_xyz': array([297.3668,  58.1152,  43.302 ]),
 'basal_dendrite.dimension.bias_xyz': array([210.4892,  93.1216,  27.7829]),
 'axon.dimension.width': 598.6509,
 'axon.dimension.height': 602.6546,
 'axon.dimension.depth': 71.5631,
 'axon.dimension.min_xyz': array([-252.2122, -220.8229,  -16.1489]),
 'axon.dimension.max_xyz': array([346.4387, 381.8317,  55.4142]),
 'axon.dimension.bias_xyz': array([ 94.2265, 161.0088,  39.2653]),
 'all_neurites.num_branches': 234,
 'dendrite.num_branches': 28,
 'basal_dendrite.num_branches': 28,
 'axon.num_branches': 205,
 'all_neurites.num_tips': 122,
 'dendrite.num_tips': 16,
 'basal_dendrite.num_tips': 16,
 'axon.num_tips': 106}
In [13]:
# ... by keeping the extractor we can re-use it on new data

run_two = extractor.extract(more_test_data)
unnest(run_two.results)
Out[13]:
{'all_neurites.num_outer_bifurcations': 7,
 'dendrite.num_outer_bifurcations': 1,
 'basal_dendrite.num_outer_bifurcations': 1,
 'axon.num_outer_bifurcations': 7,
 'all_neurites.dimension.width': 301.5069,
 'all_neurites.dimension.height': 513.0175999999999,
 'all_neurites.dimension.depth': 96.85220000000001,
 'all_neurites.dimension.min_xyz': array([-172.847 , -360.2776,  -22.221 ]),
 'all_neurites.dimension.max_xyz': array([128.6599, 152.74  ,  74.6312]),
 'all_neurites.dimension.bias_xyz': array([ 44.1871, 207.5376,  52.4102]),
 'dendrite.dimension.width': 196.23379999999997,
 'dendrite.dimension.height': 148.10680000000002,
 'dendrite.dimension.depth': 51.94,
 'dendrite.dimension.min_xyz': array([-156.0119,  -62.992 ,  -14.3438]),
 'dendrite.dimension.max_xyz': array([40.2219, 85.1148, 37.5962]),
 'dendrite.dimension.bias_xyz': array([115.79  ,  22.1228,  23.2524]),
 'basal_dendrite.dimension.width': 196.23379999999997,
 'basal_dendrite.dimension.height': 148.10680000000002,
 'basal_dendrite.dimension.depth': 51.94,
 'basal_dendrite.dimension.min_xyz': array([-156.0119,  -62.992 ,  -14.3438]),
 'basal_dendrite.dimension.max_xyz': array([40.2219, 85.1148, 37.5962]),
 'basal_dendrite.dimension.bias_xyz': array([115.79  ,  22.1228,  23.2524]),
 'axon.dimension.width': 301.5069,
 'axon.dimension.height': 513.0175999999999,
 'axon.dimension.depth': 96.85220000000001,
 'axon.dimension.min_xyz': array([-172.847 , -360.2776,  -22.221 ]),
 'axon.dimension.max_xyz': array([128.6599, 152.74  ,  74.6312]),
 'axon.dimension.bias_xyz': array([ 44.1871, 207.5376,  52.4102]),
 'all_neurites.num_branches': 183,
 'dendrite.num_branches': 36,
 'basal_dendrite.num_branches': 36,
 'axon.num_branches': 147,
 'all_neurites.num_tips': 96,
 'dendrite.num_tips': 22,
 'basal_dendrite.num_tips': 22,
 'axon.num_tips': 74}

It is easy to write and categorize new features¶

In [14]:
from neuron_morphology.feature_extractor.mark import Mark
from neuron_morphology.feature_extractor.marked_feature import marked

IsGlobal = Mark.factory("IsGlobal")
IsTrivial = Mark.factory("IsTrivial")

class DoesMath(Mark):
    """ This feature involves math! Documenting your marks can be handy.
    """


@marked(IsGlobal)
@marked(IsTrivial)
def num_nodes(data: Data) -> int:
    """Count the nodes!
    """
    return len(data.morphology.nodes)

num_nodes
Out[14]:
Signature:
num_nodes(data: neuron_morphology.feature_extractor.data.Data) -> int

Marks: ['IsGlobal', 'IsTrivial']

Requires: frozenset()

Help:
Count the nodes!
    
In [15]:
# we can also mark an existing function
num_nodes = marked(DoesMath)(num_nodes)

num_nodes
Out[15]:
Signature:
num_nodes(data: neuron_morphology.feature_extractor.data.Data) -> int

Marks: ['IsGlobal', 'DoesMath', 'IsTrivial']

Requires: frozenset()

Help:
Count the nodes!