Features

A tool for analysing of morphologies. It allows to extract various information about morphologies. For example if you need to know the segment lengths of a morphology then you need to call segment_lengths feature. The complete list of available features is spread among pages neurom.features.neurite, neurom.features.morphology, neurom.features.population.

Features are spread among neurite, morphology, population modules to emphasize their expected input. Features from neurite expect a neurite as their input. So calling it with a morphology input will fail. morphology expects a morphology only. population expects a population only.

This restriction can be bypassed if you call a feature from neurite via the features mechanism features.get. However the mechanism does not allow to appply population features to anything other than a morphology population, and morphology features can be applied only to a morphology or a morphology population.

An example for neurite:

from neurom import load_morphology, features
from neurom.features.neurite import max_radial_distance

m = load_morphology("tests/data/swc/Neuron.swc")

# valid input
rd = max_radial_distance(m.neurites[0])

# invalid input
# rd = max_radial_distance(m)

# valid input
rd = features.get('max_radial_distance', m)

The features mechanism assumes that a neurite feature must be summed if it returns a number, and concatenated if it returns a list. Other types of returns are invalid. For example lets take a feature number_of_segments of neurite. It returns a number of segments in a neurite. Calling it on a morphology will return a sum of number_of_segments of all the morphology’s neurites. Calling it on a morphology population will return a list of number_of_segments of each morphology within the population.

from neurom import load_morphology, load_morphologies, features

m = load_morphology("tests/data/swc/Neuron.swc")

# a single number
features.get('number_of_segments', m.neurites[0])

# a single number that is a sum for all `m.neurites`.
features.get('number_of_segments', m)

pop = load_morphologies("tests/data/valid_set")

# a list of numbers
features.get('number_of_segments', pop)

if a list is returned then the feature results are concatenated.

from neurom import load_morphology, load_morphologies, features

m = load_morphology("tests/data/swc/Neuron.swc")

# a list of lengths in a neurite
features.get('section_lengths', m.neurites[0])

# a flat list of lengths in a morphology, no separation among neurites
features.get('section_lengths', m)

pop = load_morphologies("tests/data/valid_set")

# a flat list of lengths in a population, no separation among morphologies
features.get('section_lengths', pop)

In case such implicit behaviour does not work a feature can be rewritten for each input separately. For example, a feature max_radial_distance that requires a max operation instead of implicit sum. Its definition in neurite:

@feature(shape=())
def max_radial_distance(neurite, origin=None, section_type=NeuriteType.all):
    """Get the maximum radial distances of the termination sections."""
    term_radial_distances = section_term_radial_distances(
        neurite, origin=origin, section_type=section_type
    )
    return max(term_radial_distances) if term_radial_distances else 0.0

In order to make it work for a morphology, it is redefined in morphology:

@feature(shape=())
def max_radial_distance(morph, origin=None, neurite_type=NeuriteType.all):
    """Get the maximum radial distances of the termination sections."""
    origin = morph.soma.center if origin is None else origin

    term_radial_distances = _map_neurites(
        partial(nf.max_radial_distance, origin=origin), morph, neurite_type
    )
    return max(term_radial_distances) if term_radial_distances else 0.0

Another feature that requires redefining is sholl_frequency. This feature applies different logic for a morphology and a morphology population. That is why it is defined in morphology:

@feature(shape=(...,))
def sholl_frequency(morph, neurite_type=NeuriteType.all, step_size=10, bins=None):
    """Perform Sholl frequency calculations on a morph.

    Args:
        morph(Morphology): a morphology
        neurite_type(NeuriteType): which neurites to operate on
        step_size(float): step size between Sholl radii
        bins: iterable of floats defining custom binning to use for the Sholl radii.
            If None, it uses intervals of step_size between min and max radii of ``morphologies``.

    Note:
        Given a morphology, the soma center is used for the concentric circles,
        which range from the soma radii, and the maximum radial distance
        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.
    """
    _assert_soma_center(morph)

    if bins is None:
        min_soma_edge = morph.soma.radius

        sections = _get_sections(morph, neurite_type)

        max_radius_per_section = [
            np.max(np.linalg.norm(section.points[:, COLS.XYZ] - morph.soma.center, axis=1))
            for section in sections
        ]

        if not max_radius_per_section:
            return []

        bins = np.arange(min_soma_edge, min_soma_edge + max(max_radius_per_section), step_size)

    return sholl_crossings(morph, neurite_type, morph.soma.center, bins)

and redefined in population

@feature(shape=(...,))
def sholl_frequency(morphs, neurite_type=NeuriteType.all, step_size=10, bins=None):
    """Perform Sholl frequency calculations on a population of morphs.

    Args:
        morphs(list|Population): list of morphologies or morphology population
        neurite_type(NeuriteType): which neurites to operate on
        step_size(float): step size between Sholl radii
        bins(Iterable[float]): custom binning to use for the Sholl radii.
            If None, it uses intervals of step_size between min and max radii of ``morphs``.
        use_subtrees (bool): Enable mixed subtree processing.

    Note:
        Given a population, the concentric circles range from the smallest soma radius to the
        largest radial neurite distance 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.
    """
    neurite_filter = is_type(neurite_type)

    if bins is None:
        section_iterator = partial(
            iter_sections, neurite_filter=neurite_filter, section_filter=neurite_filter
        )

        max_radius_per_section = [
            np.max(np.linalg.norm(section.points[:, COLS.XYZ] - morph.soma.center, axis=1))
            for morph in map(_assert_soma_center, morphs)
            for section in section_iterator(morph)
        ]

        if not max_radius_per_section:
            return []

        min_soma_edge = min(n.soma.radius for n in morphs)

        bins = np.arange(min_soma_edge, min_soma_edge + max(max_radius_per_section), step_size)

    def _sholl_crossings(morph):
        _assert_soma_center(morph)
        return mf.sholl_crossings(morph, neurite_type, morph.soma.center, bins)

    return np.array([_sholl_crossings(m) for m in morphs]).sum(axis=0).tolist()