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()